Release / verify (push) Canceled after 0s
CI / checks (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 0s
Release / test (push) Canceled after 0s
Release / publish (backend) (push) Canceled after 0s
Release / publish (frontend) (push) Canceled after 0s
Release / publish (proxy) (push) Canceled after 0s
Release / publish (updater) (push) Canceled after 0s
- **Regel-Analyse: Bereinigung (nur Ansicht):** Auf **Regelwerk › Regel-Analyse** gibt es den Tab **Bereinigung** (`?tab=cleanup`). Er listet je Firewall die
Regeln, die einen Blick wert sind: **ungenutzt** (OPNexus hat in 7/30/90/180 Tagen keinen Treffer gesehen), **verdeckt** (eine frühere Quick-Regel
trifft schon alles, was sie trifft), **doppelt** und **lange deaktiviert**. Jeder Befund trägt eine Sicherheit (**sicher / vermutlich / unbekannt**, Tooltip mit
dem Grund): „sicher“ gibt es nur mit vollständigen, frischen Belegen, bei einer Regel mit Zeitplan, ohne geladenen pf-Regelsatz oder bei Aliasen bleibt es
höchstens „vermutlich“. Die Tabelle zeigt auch den letzten Treffer und ob die Regel entfernbar wäre (nicht: automatische Regeln, Regeln im Altformat, von OPNexus
verwaltete Regeln – die entfernt man über die Regelseite). Es wird nur gelesen; **Entfernen folgt in einer späteren Version**. Die Sicherheitslage verlinkt ihre
Befunde „ungenutzt/verdeckt/doppelt“ direkt dorthin; per API `GET /api/hosts/{id}/rule-analysis/cleanup?unused_days=30`, per MCP `rule_cleanup_candidates`.
- **Änderungsverlauf je Regel:** OPNexus liest alle 15 Minuten die Regeln jeder Firewall und merkt sich, was sich geändert hat – auch bei Regeln, die nicht von OPNexus
stammen: angelegt, geändert (mit Vorher/Nachher), aktiviert, deaktiviert, entfernt, jeweils mit der Herkunft „über OPNexus“ (passender Audit-Eintrag) oder „extern“.
Auf der Live-Regel-Ansicht (Regelwerk › Live über alle Hosts) öffnet die Zeilenaktion **Verlauf** die Ereignisse zusammen mit den gespeicherten Fassungen und dem Audit der
verwalteten Regel. Die erste Beobachtung speichert nur den Ausgangszustand; der Verlauf wird 365 Tage aufbewahrt (`GET /api/hosts/{id}/live-rules/{uuid}/history`).
Eine Umsortierung (andere Reihenfolge oder Gruppe) gilt als „verschoben“, nicht als Änderung. Eine leere oder unvollständige Antwort der Firewall (ohne
automatische Regeln) wird verworfen; verschwinden mehr als die Hälfte der Regeln auf einmal, zählt erst die zweite gleiche Beobachtung. Die Herkunft „über OPNexus“
erklärt nur Audit-Einträge seit der vorherigen Beobachtung, und jeder Eintrag nur eine Änderung.
Schemaänderungen 0027 und 0028 (Tabellen und ein Feld für den Beobachtungszeitraum) laufen beim Start.
- **Regelnutzung genauer:** Die Zähler kommen jetzt aus `ruleStats` (von der Firewall schon je Regel summiert; ältere Versionen weiterhin über den pf-Regelsatz) und werden alle
15 Minuten statt alle 6 Stunden gelesen. Regeln im Altformat (noch nicht migriert) werden ebenfalls erfasst. Das Beobachtungsfenster für „ungenutzt“ beginnt neu, wenn eine Regel eingeschaltet oder geändert wird, und nach einem pf-Neuladen mit langer Lücke bei den Zählern.
- **Verdeckt/doppelt in der Sicherheitslage genauer:** Der Vergleich kennt jetzt die echte Reihenfolge (Floating vor Interface-Gruppen vor Interface-Regeln), Netze und
Portbereiche als Mengen (z. B. 10.1.0.0/16 liegt in 10.0.0.0/8), `inet46` und `tcp/udp`, und übergeht Regeln mit Zeitplan, Markierung oder Limits. Es findet dadurch
mehr verdeckte Regeln als bisher, nie weniger belegte.
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import { X } from "lucide-react";
|
|
import { ReactNode, useEffect, useId, useRef } from "react";
|
|
import { useLanguage } from "@/components/LanguageProvider";
|
|
|
|
interface ModalProps {
|
|
title: string;
|
|
onClose: () => void;
|
|
children: ReactNode;
|
|
/** Wider dialog for lists and tables. */
|
|
wide?: boolean;
|
|
}
|
|
|
|
export function Modal({ title, onClose, children, wide = false }: ModalProps) {
|
|
const titleId = useId();
|
|
const { t } = useLanguage();
|
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Keyboard users must not tab out into the page behind the dialog, and
|
|
// focus should return to the trigger afterwards so they keep their place.
|
|
useEffect(() => {
|
|
const previous = document.activeElement as HTMLElement | null;
|
|
const dialog = dialogRef.current;
|
|
if (dialog && !dialog.contains(document.activeElement)) {
|
|
(dialog.querySelector<HTMLElement>("input, textarea, select") ?? dialog).focus();
|
|
}
|
|
function trap(e: KeyboardEvent) {
|
|
if (e.key !== "Tab" || !dialog) return;
|
|
const items = Array.from(dialog.querySelectorAll<HTMLElement>("a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex='-1'])"));
|
|
if (items.length === 0) return;
|
|
const first = items[0];
|
|
const last = items[items.length - 1];
|
|
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
}
|
|
document.addEventListener("keydown", trap);
|
|
return () => {
|
|
document.removeEventListener("keydown", trap);
|
|
previous?.focus?.();
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if (e.key === "Escape") onClose();
|
|
}
|
|
window.addEventListener("keydown", onKeyDown);
|
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
}, [onClose]);
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
|
<div
|
|
ref={dialogRef}
|
|
tabIndex={-1}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby={titleId}
|
|
className={`flex max-h-[90vh] outline-none w-full ${wide ? "max-w-2xl" : "max-w-lg"} flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-xl sm:p-6`}
|
|
>
|
|
<div className="mb-4 flex shrink-0 items-center justify-between">
|
|
<h2 id={titleId} className="text-lg font-semibold text-slate-900">
|
|
{title}
|
|
</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="rounded-lg p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600"
|
|
aria-label={t("common.close")} title={t("common.close")}
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
<div className="overflow-y-auto">{children}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|