Quelltext des aktuellen Standes
<?php
$this->layout('layouts/main', [
'page_title' => $page_title,
'current_route' => 'xdr',
'full_width' => true,
]);
$alerts = $alerts ?? [];
$kpis = $kpis ?? ['open' => 0, 'severe' => 0, 'unmatched' => 0, 'last_sync' => null];
$providers = $providers ?? [];
$filter = $filter ?? ['provider' => '', 'severity' => '', 'status' => '', 'match' => '', 'q' => ''];
$canManage = $can_manage ?? false;
$page = $page ?? 1;
$pages = $pages ?? 1;
$total = $total ?? 0;
$view = $this;
$sevPill = [
'critical' => 'status-pill--danger',
'high' => 'status-pill--orange',
'medium' => 'status-pill--warning',
'low' => 'status-pill--info',
'info' => 'status-pill--neutral',
];
$sevLabel = [
'critical' => 'Kritisch', 'high' => 'Hoch', 'medium' => 'Mittel',
'low' => 'Niedrig', 'info' => 'Info',
];
$statusPill = [
'new' => 'status-pill--info',
'investigating' => 'status-pill--warning',
'acknowledged' => 'status-pill--purple',
'resolved' => 'status-pill--success',
'dismissed' => 'status-pill--neutral',
];
$statusLabel = [
'new' => 'Neu', 'investigating' => 'In Bearbeitung', 'acknowledged' => 'Quittiert',
'resolved' => 'Erledigt', 'dismissed' => 'Verworfen',
];
$providerLabel = ['trendmicro' => 'Trend Vision One', 'defender' => 'Microsoft Defender XDR'];
?>
<style>
/* XDR-Modul – modul-spezifische Ergänzungen (Prefix xdr-).
Standardkomponenten (page-body, kpi-row, small-box, table-wrapper,
data-table, status-pill, subnav, empty-state, filter-card-body) aus app.css. */
.xdr-subnav { flex:1 1 100%; margin-bottom:0; }
.xdr-filterbar { display:flex; flex-wrap:wrap; gap:.5rem; align-items:center; flex:1 1 100%; width:100%; }
.xdr-mitre { display:inline-block; background:var(--gray-100); border:1px solid var(--gray-200); border-radius:4px; font-size:.7rem; padding:.05rem .4rem; color:var(--gray-700); margin:.05rem .1rem; }
.xdr-unassigned { font-weight:600; }
.xdr-title-cell { max-width:340px; }
.xdr-title-cell small { color:var(--gray-500); display:block; }
</style>
<div class="content-header">
<div>
<h1><?= $this->te('XDR-Sicherheitswarnungen') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item active">XDR</li>
</ol>
</div>
</div>
<div class="page-body">
<div class="kpi-row">
<div class="kpi-col">
<a href="/xdr">
<div class="small-box bg-info">
<div class="inner"><h3><?= (int)$kpis['open'] ?></h3><p><?= $this->te('Offene Warnungen') ?></p></div>
<div class="icon"><i class="fas fa-shield-virus"></i></div>
</div>
</a>
</div>
<div class="kpi-col">
<a href="/xdr?severity=critical">
<div class="small-box bg-danger">
<div class="inner"><h3><?= (int)$kpis['severe'] ?></h3><p><?= $this->te('Kritisch / Hoch') ?></p></div>
<div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
</div>
</a>
</div>
<div class="kpi-col">
<a href="/xdr?focus=unassigned">
<div class="small-box bg-warning">
<div class="inner"><h3><?= (int)$kpis['unmatched'] ?></h3><p><?= $this->te('Nicht zugeordnet') ?></p></div>
<div class="icon"><i class="fas fa-question-circle"></i></div>
</div>
</a>
</div>
<div class="kpi-col">
<div class="small-box bg-secondary">
<div class="inner" style="font-size:.95rem;">
<h3 style="font-size:1.1rem;"><?= $kpis['last_sync'] ? $view->e(date('d.m.Y H:i', strtotime($kpis['last_sync'] . ' UTC'))) : '–' ?></h3>
<p><?= $this->te('Letzter Abgleich') ?></p>
</div>
<div class="icon"><i class="fas fa-sync"></i></div>
</div>
</div>
</div>
<!-- Filter-Card: Ansichten-Tabs + Filter (wie /orders, /status) -->
<div class="card card-outline card-primary mb-3">
<div class="filter-card-body">
<div class="subnav xdr-subnav">
<a href="/xdr" class="subnav-tab <?= $filter['match'] === '' ? 'active' : '' ?>"><?= $this->te('Alle Warnungen') ?></a>
<a href="/xdr?focus=unassigned" class="subnav-tab <?= $filter['match'] === 'unmatched' ? 'active' : '' ?>"><?= $this->te('Nicht zugeordnet') ?></a>
</div>
<form method="get" action="/xdr" class="xdr-filterbar">
<div class="filter-input-wrap" style="flex:1;min-width:200px;">
<svg class="filter-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="search" name="q" class="form-control form-control-sm" value="<?= $view->e($filter['q']) ?>" placeholder="<?= $this->te('Gerät oder Titel…') ?>">
</div>
<select name="provider" class="form-control form-control-sm" style="width:auto;">
<option value=""><?= $this->te('Alle Anbieter') ?></option>
<?php foreach ($providers as $p): ?>
<option value="<?= $view->e($p) ?>" <?= $filter['provider'] === $p ? 'selected' : '' ?>>
<?= $view->e($providerLabel[$p] ?? $p) ?>
</option>
<?php endforeach; ?>
</select>
<select name="severity" class="form-control form-control-sm" style="width:auto;">
<option value=""><?= $this->te('Alle Schweregrade') ?></option>
<?php foreach ($sevLabel as $k => $lbl): ?>
<option value="<?= $k ?>" <?= $filter['severity'] === $k ? 'selected' : '' ?>><?= $lbl ?></option>
<?php endforeach; ?>
</select>
<select name="status" class="form-control form-control-sm" style="width:auto;">
<option value=""><?= $this->te('Alle Status') ?></option>
<?php foreach ($statusLabel as $k => $lbl): ?>
<option value="<?= $k ?>" <?= $filter['status'] === $k ? 'selected' : '' ?>><?= $lbl ?></option>
<?php endforeach; ?>
</select>
<select name="match" class="form-control form-control-sm" style="width:auto;">
<option value=""><?= $this->te('Alle Zuordnungen') ?></option>
<option value="matched" <?= $filter['match'] === 'matched' ? 'selected' : '' ?>><?= $this->te('Zugeordnet') ?></option>
<option value="unmatched" <?= $filter['match'] === 'unmatched' ? 'selected' : '' ?>><?= $this->te('Nicht zugeordnet') ?></option>
<option value="ambiguous" <?= $filter['match'] === 'ambiguous' ? 'selected' : '' ?>><?= $this->te('Mehrdeutig') ?></option>
</select>
<button type="submit" class="btn btn-primary btn-sm"><?= $this->te('Filtern') ?></button>
<a href="/xdr" class="btn btn-default btn-sm"><?= $this->te('Zurücksetzen') ?></a>
</form>
</div>
</div>
<div class="table-wrapper">
<?php if (empty($alerts)): ?>
<div class="empty-state">
<p><?= $this->t('Keine Warnungen gefunden. Konfiguriere unter <a href="/admin/xdr/settings">XDR-Einstellungen</a> einen Anbieter, um Warnungen zu importieren.') ?></p>
</div>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th scope="col"><?= $this->te('Schweregrad') ?></th>
<th scope="col"><?= $this->te('Warnung') ?></th>
<th scope="col"><?= $this->te('Gerät / Asset') ?></th>
<th scope="col"><?= $this->te('Anbieter') ?></th>
<th scope="col"><?= $this->te('Status') ?></th>
<th scope="col"><?= $this->te('Erkannt') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($alerts as $a): ?>
<?php
$sev = (string)$a['severity'];
$st = (string)$a['local_status'];
?>
<tr>
<td>
<span class="status-pill <?= $sevPill[$sev] ?? 'status-pill--neutral' ?>">
<?= $sevLabel[$sev] ?? $view->e($sev) ?>
</span>
</td>
<td class="xdr-title-cell">
<a href="/xdr/<?= (int)$a['id'] ?>" class="table-link"><?= $view->e($a['title']) ?></a>
<?php if (($a['alert_type'] ?? '') === 'oat'): ?>
<span class="badge">OAT</span>
<?php endif; ?>
</td>
<td>
<?php if (!empty($a['asset_id'])): ?>
<a href="/inventory/<?= (int)$a['asset_id'] ?>" class="table-link">
<?= $view->e($a['asset_name'] ?? ('#' . $a['asset_id'])) ?>
</a>
<?php elseif (($a['match_status'] ?? '') === 'ambiguous'): ?>
<span class="status-pill status-pill--orange"><?= $this->te('Mehrdeutig') ?></span>
<small><?= $view->e($a['endpoint_name'] ?? '') ?></small>
<?php else: ?>
<span class="status-pill status-pill--danger xdr-unassigned"><?= $this->te('Nicht zugeordnet') ?></span>
<small><?= $view->e($a['endpoint_name'] ?? '–') ?></small>
<?php endif; ?>
</td>
<td><?= $view->e($providerLabel[$a['provider']] ?? $a['provider']) ?></td>
<td>
<span class="status-pill <?= $statusPill[$st] ?? 'status-pill--neutral' ?>">
<?= $statusLabel[$st] ?? $view->e($st) ?>
</span>
<?php if (!empty($a['assigned_to_name'])): ?>
<br><small><i class="fas fa-user-check"></i> <?= $view->e((string)$a['assigned_to_name']) ?></small>
<?php endif; ?>
</td>
<td><?= $a['detected_at'] ? $view->e(date('d.m.Y H:i', strtotime((string)$a['detected_at']))) : '–' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php if ($pages > 1): ?>
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:.75rem;">
<small><?= $this->te('{anzahl} Warnung(en)', ['anzahl' => (int)$total]) ?></small>
<div>
<?php
$qs = $filter;
$mk = function (int $p) use ($qs): string {
$qs['page'] = $p;
return '/xdr?' . http_build_query(array_filter($qs, fn($v) => $v !== '' && $v !== null));
};
?>
<?php if ($page > 1): ?><a class="btn btn-default btn-sm" href="<?= $mk($page - 1) ?>"><?= $this->te('« Zurück') ?></a><?php endif; ?>
<span style="margin:0 .5rem;"><?= $this->te('Seite {seite} / {gesamt}', ['seite' => (int)$page, 'gesamt' => (int)$pages]) ?></span>
<?php if ($page < $pages): ?><a class="btn btn-default btn-sm" href="<?= $mk($page + 1) ?>"><?= $this->te('Weiter »') ?></a><?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>