Compare commits
32 Commits
32ab2c7f47
...
v1.3.27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95f2238588 | ||
|
|
de83936fce | ||
|
|
3215da8a84 | ||
|
|
6f69697705 | ||
|
|
33cfc1a90d | ||
|
|
3e05c7fe49 | ||
|
|
924540d7a9 | ||
|
|
561816d79d | ||
|
|
cab78eb3d1 | ||
|
|
f61f82d36f | ||
|
|
7aa2a907d5 | ||
|
|
99df6f731d | ||
|
|
bab82f8d5b | ||
|
|
7ff6575790 | ||
|
|
51e5fe83d9 | ||
|
|
b70db4ccf0 | ||
|
|
f1f7df74f7 | ||
|
|
de153dc13a | ||
|
|
5c268425c1 | ||
|
|
0846eaa05b | ||
|
|
d395e3ea68 | ||
|
|
f0be5be496 | ||
|
|
235b5c3b9a | ||
|
|
37f729381d | ||
|
|
088910ee19 | ||
|
|
09e6e0c4f7 | ||
|
|
18ee243a44 | ||
|
|
4856779db8 | ||
|
|
8e4759ccc6 | ||
|
|
96290253c8 | ||
|
|
dca40761a4 | ||
|
|
0ac91a7c59 |
42
Makefile
42
Makefile
@@ -65,7 +65,10 @@ test-race:
|
||||
CGO_ENABLED=1 $(GO) test $(GOFLAGS) -race ./...
|
||||
|
||||
GOBIN := $(shell $(GO) env GOPATH)/bin
|
||||
GOLANGCI_VERSION := v2.12.2
|
||||
GOLANGCI_VERSION := v2.13.2
|
||||
# Ziel-Go-Version aus go.mod — golangci-lint MUSS mit genau dieser Toolchain
|
||||
# gebaut sein (siehe golangci-Target).
|
||||
GO_VERSION := $(shell awk '/^go /{print $$2; exit}' go.mod)
|
||||
|
||||
lint:
|
||||
$(GO) vet ./...
|
||||
@@ -74,15 +77,33 @@ lint:
|
||||
# golangci-lint — HARTER Gate. Tool wird bei Bedarf auf pinned Version
|
||||
# installiert; bricht ab, sobald ein Finding auftaucht (Bestand ist 0,
|
||||
# Rollout abgeschlossen — siehe .golangci.yml).
|
||||
# Befund 2026-09-11: golangci-lint verweigert den Dienst ("the Go language
|
||||
# version used to build golangci-lint is lower than the targeted Go version"),
|
||||
# sobald go.mod eine neuere Go-Version zielt als die, mit der der Linter
|
||||
# gebaut wurde — und ein `go install` ohne GOTOOLCHAIN baut ihn mit der in
|
||||
# SEINER go.mod geforderten (älteren) Version. Deshalb die Projekt-Toolchain
|
||||
# pinnen. Der frühere `command -v`-Check hat ausserdem eine bereits
|
||||
# installierte, veraltete Binary nie erneuert; jetzt entscheidet die
|
||||
# tatsächliche Version + Build-Go-Version der Binary über die Neuinstallation.
|
||||
golangci:
|
||||
@command -v golangci-lint >/dev/null 2>&1 || GOFLAGS= $(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION)
|
||||
@if ! "$(GOBIN)/golangci-lint" version 2>/dev/null | grep -q "has version $(patsubst v%,%,$(GOLANGCI_VERSION)) built with go$(GO_VERSION) "; then \
|
||||
echo " -> installing golangci-lint $(GOLANGCI_VERSION) (built with go$(GO_VERSION))"; \
|
||||
GOFLAGS= GOTOOLCHAIN=go$(GO_VERSION) $(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION); \
|
||||
fi
|
||||
@PATH="$(GOBIN):$$PATH" golangci-lint run --timeout 6m
|
||||
|
||||
# govulncheck — Go-Vuln-DB-Scan. HARTER Release-Gate: bricht ab, wenn der
|
||||
# Code eine bekannte Vulnerability tatsächlich aufruft. Tool wird bei Bedarf
|
||||
# automatisch installiert.
|
||||
# Gleiche Toolchain-Falle wie bei golangci-lint (Befund 2026-09-11): ein
|
||||
# `go install` ohne GOTOOLCHAIN baut govulncheck mit einer aelteren Go-Version,
|
||||
# die dann "package requires newer Go version" fuer jedes Paket meldet statt zu
|
||||
# scannen. Reinstall, sobald die Build-Go-Version der Binary abweicht.
|
||||
vulncheck:
|
||||
@command -v govulncheck >/dev/null 2>&1 || GOFLAGS= $(GO) install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
@if ! "$(GOBIN)/govulncheck" -version 2>/dev/null | grep -q "^Go: go$(GO_VERSION)$$"; then \
|
||||
echo " -> installing govulncheck (built with go$(GO_VERSION))"; \
|
||||
GOFLAGS= GOTOOLCHAIN=go$(GO_VERSION) $(GO) install golang.org/x/vuln/cmd/govulncheck@latest; \
|
||||
fi
|
||||
@PATH="$(GOBIN):$$PATH" govulncheck ./...
|
||||
|
||||
# Go-Quality-Baseline — läuft automatisch vor jedem Release (deb/publish).
|
||||
@@ -104,7 +125,7 @@ ui:
|
||||
@echo " -> management-ui (vite build, version $(VERSION))"
|
||||
@cd management-ui && \
|
||||
if [ -x "$$(command -v bun)" ]; then bun install --silent && bun run build; \
|
||||
else npm install --silent && npm run build; fi
|
||||
else npm install --include=dev --silent && npm run build; fi
|
||||
|
||||
deb-amd64: release-check build-linux-amd64 ui
|
||||
@./scripts/apt-repo/build-package.sh amd64 $(VERSION)
|
||||
@@ -114,17 +135,18 @@ deb-arm64: release-check build-linux-arm64 ui
|
||||
|
||||
deb: deb-amd64 deb-arm64
|
||||
|
||||
GITEA_DEB_URL := https://git.netcell-it.de/api/packages/projekte/debian/pool/trixie/main/upload
|
||||
|
||||
# Direktes `make publish` bleibt als Handnotbremse erhalten, veröffentlicht
|
||||
# aber immer nach stable — für Testing-Releases + das Stable-Promotion-
|
||||
# Gate (verify_channel_debs, Version-Bump, Git-Tag) scripts/release.sh nutzen.
|
||||
publish-amd64: deb-amd64
|
||||
@./scripts/apt-repo/publish.sh $(VERSION) amd64
|
||||
@./scripts/apt-repo/publish.sh $(VERSION) amd64 stable
|
||||
@echo " -> cleanup-old (keep last $${KEEP:-10})"
|
||||
@./scripts/apt-repo/cleanup-old.sh
|
||||
@./scripts/apt-repo/cleanup-old.sh stable
|
||||
|
||||
publish-arm64: deb-arm64
|
||||
@./scripts/apt-repo/publish.sh $(VERSION) arm64
|
||||
@./scripts/apt-repo/publish.sh $(VERSION) arm64 stable
|
||||
@echo " -> cleanup-old (keep last $${KEEP:-10})"
|
||||
@./scripts/apt-repo/cleanup-old.sh
|
||||
@./scripts/apt-repo/cleanup-old.sh stable
|
||||
|
||||
publish: publish-amd64 publish-arm64
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/cluster"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/cluster/clustertls"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/cluster/jointoken"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/crowdsec"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/database"
|
||||
firewallrender "git.netcell-it.de/projekte/edgeguard-native/internal/firewall"
|
||||
radiusrender "git.netcell-it.de/projekte/edgeguard-native/internal/freeradius"
|
||||
@@ -307,6 +309,7 @@ func main() {
|
||||
"unbound": unboundrender.New(pool).RenderToString,
|
||||
"chrony": chronyrender.New(pool).RenderToString,
|
||||
"wireguard": wgrender.New(pool, secretsBox).RenderToString,
|
||||
"crowdsec-whitelist": crowdsec.NewWhitelistGenerator(pool).RenderToString,
|
||||
})
|
||||
setupHdl.WithAudit(auditRepo, nodeID)
|
||||
setupHdl.WithClusterSupport(clusterStore, func(ctx context.Context) error {
|
||||
@@ -323,6 +326,14 @@ func main() {
|
||||
return haproxy.New(pool).Render(ctx)
|
||||
}
|
||||
|
||||
// Domain-Mutationen rendern zusätzlich die CrowdSec-Admin-Whitelist neu
|
||||
// (Flag crowdsec_trusted → host-genaue Ausnahme). No-op ohne CrowdSec.
|
||||
// Beide laufen unabhängig; Fehler werden zusammengefasst (nur geloggt).
|
||||
crowdsecWL := crowdsec.NewWhitelistGenerator(pool)
|
||||
domainsReloader := func(ctx context.Context) error {
|
||||
return errors.Join(haproxy.New(pool).Render(ctx), crowdsecWL.Render(ctx))
|
||||
}
|
||||
|
||||
authed := v1.Group("")
|
||||
authed.Use(requireAuth, handlers.RequireAdminForMutations())
|
||||
setupHdl.RegisterAuthed(authed)
|
||||
@@ -335,7 +346,7 @@ func main() {
|
||||
WithAudit(auditRepo, nodeID)
|
||||
oidcHdl.RegisterPublic(v1)
|
||||
oidcHdl.RegisterAdmin(authed)
|
||||
handlers.NewDomainsHandler(domainsRepo, routingRepo, domainHeadersRepo, auditRepo, nodeID, haproxyReloader).Register(authed)
|
||||
handlers.NewDomainsHandler(domainsRepo, routingRepo, domainHeadersRepo, auditRepo, nodeID, domainsReloader).Register(authed)
|
||||
handlers.NewBackendsHandler(backendsRepo, auditRepo, nodeID, haproxyReloader).Register(authed)
|
||||
handlers.NewBackendServersHandler(backendServersRepo, auditRepo, nodeID, haproxyReloader).Register(authed)
|
||||
handlers.NewRoutingRulesHandler(routingRepo, auditRepo, nodeID, haproxyReloader).Register(authed)
|
||||
|
||||
230
cmd/edgeguard-ctl/cluster_reconcile.go
Normal file
230
cmd/edgeguard-ctl/cluster_reconcile.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// cmdClusterReconcileReplication bringt Publication, Grants und Subscription
|
||||
// idempotent in den Soll-Zustand. Verhindert die zwei Fehlermodi, die sich
|
||||
// beim Nachrüsten von Features zeigen:
|
||||
// - Eine `FOR TABLE`-Publication nimmt später per Migration hinzugekommene
|
||||
// Shared-Tables NICHT automatisch auf → sie replizieren nie (Standby
|
||||
// läuft nach Failover ohne WAF/OIDC/DHCP/RADIUS-Config).
|
||||
// - Der GRANT SELECT für den Replikations-User ist ein Snapshot bei Setup;
|
||||
// neue Tabellen fehlen → tablesync hängt in `d` (Permission).
|
||||
//
|
||||
// Rollen-Selbsterkennung (idempotent, läuft im postinst nach migrate):
|
||||
//
|
||||
// Publisher (hat Publication):
|
||||
// - GRANT SELECT auf ALLE Tabellen (+ DEFAULT PRIVILEGES) für den
|
||||
// Replikations-User.
|
||||
// - Publication-Mitgliedschaft angleichen: fehlende Shared-Tables ADD,
|
||||
// fälschlich enthaltene node-lokale (localOnlyTables) DROP.
|
||||
// Subscriber (hat Subscription):
|
||||
// - Frisch zu synchronisierende Shared-Tables lokal TRUNCATE (Primary =
|
||||
// Source of Truth; verhindert Duplicate-Key beim Initial-COPY einer
|
||||
// per Migration seed-befüllten Singleton-Tabelle), dann REFRESH.
|
||||
// Single-Node (weder noch): nichts zu tun.
|
||||
//
|
||||
// Best-effort: Fehler werden geloggt, brechen aber ein Paket-Upgrade nie ab.
|
||||
func cmdClusterReconcileReplication(_ []string) int {
|
||||
hasPub := psqlDBBool("edgeguard",
|
||||
fmt.Sprintf("SELECT EXISTS(SELECT 1 FROM pg_publication WHERE pubname='%s')", egPubName))
|
||||
hasSub := psqlDBBool("edgeguard",
|
||||
fmt.Sprintf("SELECT EXISTS(SELECT 1 FROM pg_subscription WHERE subname='%s')", egSubName))
|
||||
|
||||
switch {
|
||||
case hasPub:
|
||||
reconcilePublisher()
|
||||
case hasSub:
|
||||
reconcileSubscriber()
|
||||
default:
|
||||
// Standalone-Node — keine Replikation eingerichtet.
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// reconcilePublisher gleicht Grants + Publication-Mitgliedschaft an.
|
||||
func reconcilePublisher() {
|
||||
// 1. Grants IMMER neu setzen (idempotent, deckt neue Tabellen ab).
|
||||
grantSQL := fmt.Sprintf(
|
||||
"GRANT SELECT ON ALL TABLES IN SCHEMA public TO %s;\n"+
|
||||
"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO %s;",
|
||||
egReplUser, egReplUser)
|
||||
if err := psqlDBExec("edgeguard", grantSQL); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "reconcile: GRANT SELECT fehlgeschlagen:", err)
|
||||
} else {
|
||||
fmt.Printf("✓ reconcile: SELECT-Grants für %q aktualisiert\n", egReplUser)
|
||||
}
|
||||
|
||||
// 2. Publication-Mitgliedschaft angleichen.
|
||||
desired, err := desiredSharedTables()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "reconcile: Tabellen-Liste:", err)
|
||||
return
|
||||
}
|
||||
current, err := publicationTables()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "reconcile: Publication-Liste:", err)
|
||||
return
|
||||
}
|
||||
desiredSet := toSet(desired)
|
||||
currentSet := toSet(current)
|
||||
|
||||
var toAdd, toDrop []string
|
||||
for _, t := range desired {
|
||||
if !currentSet[t] {
|
||||
toAdd = append(toAdd, t)
|
||||
}
|
||||
}
|
||||
for _, t := range current {
|
||||
if !desiredSet[t] {
|
||||
toDrop = append(toDrop, t) // node-lokale, die fälschlich drin sind
|
||||
}
|
||||
}
|
||||
sort.Strings(toAdd)
|
||||
sort.Strings(toDrop)
|
||||
|
||||
if len(toAdd) > 0 {
|
||||
if err := psqlDBExec("edgeguard", fmt.Sprintf(
|
||||
"ALTER PUBLICATION %s ADD TABLE %s;", egPubName, strings.Join(toAdd, ", "))); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "reconcile: ADD TABLE fehlgeschlagen:", err)
|
||||
} else {
|
||||
fmt.Printf("✓ reconcile: %d Tabelle(n) zur Publication hinzugefügt: %s\n",
|
||||
len(toAdd), strings.Join(toAdd, ", "))
|
||||
}
|
||||
}
|
||||
if len(toDrop) > 0 {
|
||||
if err := psqlDBExec("edgeguard", fmt.Sprintf(
|
||||
"ALTER PUBLICATION %s DROP TABLE %s;", egPubName, strings.Join(toDrop, ", "))); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "reconcile: DROP TABLE fehlgeschlagen:", err)
|
||||
} else {
|
||||
fmt.Printf("✓ reconcile: %d node-lokale Tabelle(n) aus Publication entfernt: %s\n",
|
||||
len(toDrop), strings.Join(toDrop, ", "))
|
||||
}
|
||||
}
|
||||
if len(toAdd) == 0 && len(toDrop) == 0 {
|
||||
fmt.Println("✓ reconcile: Publication bereits im Soll-Zustand")
|
||||
}
|
||||
}
|
||||
|
||||
// reconcileSubscriber zieht neu publizierte Tabellen nach: erst lokal leeren
|
||||
// (Primary = Source of Truth, verhindert Duplicate-Key beim Initial-COPY),
|
||||
// dann REFRESH PUBLICATION. Bereits synchronisierte Tabellen bleiben unberührt.
|
||||
func reconcileSubscriber() {
|
||||
desired, err := desiredSharedTables()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "reconcile: Tabellen-Liste:", err)
|
||||
return
|
||||
}
|
||||
synced, err := subscriptionRelTables()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "reconcile: subscription_rel-Liste:", err)
|
||||
return
|
||||
}
|
||||
syncedSet := toSet(synced)
|
||||
|
||||
var fresh []string
|
||||
for _, t := range desired {
|
||||
if !syncedSet[t] {
|
||||
fresh = append(fresh, t)
|
||||
}
|
||||
}
|
||||
sort.Strings(fresh)
|
||||
|
||||
if len(fresh) > 0 {
|
||||
// Nur frisch zu synchronisierende Shared-Tables leeren — nie eine
|
||||
// bereits replizierte oder node-lokale Tabelle.
|
||||
if err := psqlDBExec("edgeguard",
|
||||
fmt.Sprintf("TRUNCATE %s;", strings.Join(fresh, ", "))); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "reconcile: TRUNCATE (neue Tabellen) fehlgeschlagen:", err)
|
||||
} else {
|
||||
fmt.Printf("✓ reconcile: %d neue Tabelle(n) für Initial-Sync geleert: %s\n",
|
||||
len(fresh), strings.Join(fresh, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// REFRESH ist NICHT transaktionssicher → einzelnes Statement, autocommit.
|
||||
if err := psqlDBExec("edgeguard",
|
||||
fmt.Sprintf("ALTER SUBSCRIPTION %s REFRESH PUBLICATION;", egSubName)); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "reconcile: REFRESH PUBLICATION fehlgeschlagen:", err)
|
||||
} else {
|
||||
fmt.Printf("✓ reconcile: Subscription %q refresht\n", egSubName)
|
||||
}
|
||||
}
|
||||
|
||||
// desiredSharedTables = alle public-Tabellen minus localOnlyTables.
|
||||
func desiredSharedTables() ([]string, error) {
|
||||
out, err := psqlDBRun("edgeguard", []string{"-tA", "-c",
|
||||
`SELECT tablename FROM pg_tables WHERE schemaname='public'`})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list tables: %w", err)
|
||||
}
|
||||
return filterSharedTables(splitLines(string(out))), nil
|
||||
}
|
||||
|
||||
// filterSharedTables entfernt localOnlyTables aus der Tabellenliste. Pure
|
||||
// Funktion — unit-testbar.
|
||||
func filterSharedTables(all []string) []string {
|
||||
excluded := toSet(localOnlyTables)
|
||||
var out []string
|
||||
for _, t := range all {
|
||||
if t != "" && !excluded[t] {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// publicationTables listet die aktuell in edgeguard_shared publizierten Tabellen.
|
||||
func publicationTables() ([]string, error) {
|
||||
out, err := psqlDBRun("edgeguard", []string{"-tA", "-c",
|
||||
fmt.Sprintf("SELECT tablename FROM pg_publication_tables WHERE pubname='%s'", egPubName)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return splitLines(string(out)), nil
|
||||
}
|
||||
|
||||
// subscriptionRelTables listet die Tabellen, die die Subscription bereits kennt.
|
||||
func subscriptionRelTables() ([]string, error) {
|
||||
out, err := psqlDBRun("edgeguard", []string{"-tA", "-c",
|
||||
fmt.Sprintf(`SELECT c.relname FROM pg_subscription_rel r
|
||||
JOIN pg_class c ON c.oid = r.srrelid
|
||||
JOIN pg_subscription s ON s.oid = r.srsubid
|
||||
WHERE s.subname = '%s'`, egSubName)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return splitLines(string(out)), nil
|
||||
}
|
||||
|
||||
func psqlDBBool(db, sql string) bool {
|
||||
out, err := psqlDBRun(db, []string{"-tA", "-c", sql})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(string(out)) == "t"
|
||||
}
|
||||
|
||||
func splitLines(s string) []string {
|
||||
var out []string
|
||||
for _, l := range strings.Split(strings.TrimSpace(s), "\n") {
|
||||
if l = strings.TrimSpace(l); l != "" {
|
||||
out = append(out, l)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toSet(items []string) map[string]bool {
|
||||
m := make(map[string]bool, len(items))
|
||||
for _, it := range items {
|
||||
m[it] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
27
cmd/edgeguard-ctl/cluster_reconcile_test.go
Normal file
27
cmd/edgeguard-ctl/cluster_reconcile_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFilterSharedTables(t *testing.T) {
|
||||
all := []string{
|
||||
"backends", "domains", "waf_configs", "oidc_settings",
|
||||
"ip_addresses", "network_interfaces", "alert_events", "waf_alerts",
|
||||
"ha_nodes", "goose_db_version", "radius_users", "",
|
||||
}
|
||||
got := filterSharedTables(all)
|
||||
want := []string{"backends", "domains", "oidc_settings", "radius_users", "waf_configs"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("filterSharedTables()\n got=%v\nwant=%v", got, want)
|
||||
}
|
||||
// node-lokale müssen raus sein (inkl. der frisch node-lokal gemachten).
|
||||
for _, local := range []string{"ip_addresses", "network_interfaces", "alert_events", "waf_alerts", "ha_nodes", "goose_db_version"} {
|
||||
for _, g := range got {
|
||||
if g == local {
|
||||
t.Errorf("localOnly-Tabelle %q darf NICHT in shared-Liste sein", local)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ var localOnlyTables = []string{
|
||||
"join_tokens_used", // Token-Tracking nur auf Primary relevant
|
||||
"audit_log", // Lokales Audit-Protokoll
|
||||
"alert_events", // Lokale Laufzeit-Events
|
||||
"waf_alerts", // Lokale WAF-Detection-Events (wie alert_events)
|
||||
"backups", // Backup-Historie ist per-Node
|
||||
"goose_db_version", // Migration-Tracking, internes Tool-State
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ func main() {
|
||||
os.Exit(cmdClusterInitReplication(os.Args[2:]))
|
||||
case "cluster-setup-standby":
|
||||
os.Exit(cmdClusterSetupStandby(os.Args[2:]))
|
||||
case "cluster-reconcile-replication":
|
||||
os.Exit(cmdClusterReconcileReplication(os.Args[2:]))
|
||||
case "promote":
|
||||
os.Exit(cmdPromote(os.Args[2:]))
|
||||
case "cluster-leave", "dump-config":
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/chrony"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/cluster"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/configgen"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/crowdsec"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/database"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/firewall"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/freeradius"
|
||||
@@ -65,6 +66,7 @@ func cmdRenderConfig(args []string) int {
|
||||
cn := chrony.New(pool)
|
||||
ke := kea.New(pool)
|
||||
fr := freeradius.New(pool, secrets.New(""))
|
||||
cw := crowdsec.NewWhitelistGenerator(pool)
|
||||
if skipReload {
|
||||
hap.SkipReload = true
|
||||
fw.SkipReload = true
|
||||
@@ -74,6 +76,7 @@ func cmdRenderConfig(args []string) int {
|
||||
cn.SkipReload = true
|
||||
ke.SkipReload = true
|
||||
fr.SkipReload = true
|
||||
cw.SkipReload = true
|
||||
}
|
||||
|
||||
// keepalived: Node-ID aus node.conf für Prioritäts-Berechnung
|
||||
@@ -82,7 +85,7 @@ func cmdRenderConfig(args []string) int {
|
||||
ka = keepalived.New(pool, lc.NodeID)
|
||||
}
|
||||
|
||||
gens := []configgen.Generator{hap, fw, sq, wg, ub, cn, ke, fr}
|
||||
gens := []configgen.Generator{hap, fw, sq, wg, ub, cn, ke, fr, cw}
|
||||
if ka != nil {
|
||||
gens = append(gens, ka)
|
||||
}
|
||||
|
||||
@@ -104,6 +104,12 @@ const (
|
||||
auditCleanupInterval = 24 * time.Hour
|
||||
auditRetentionDays = 90
|
||||
|
||||
// alertRetentionDays — alert_events wächst sonst unbegrenzt (node-lokale
|
||||
// Health-Events: backend.down, mem.high, cert.expiring …). Läuft im
|
||||
// selben täglichen Tick wie der Audit-Cleanup. Fester Default, kein
|
||||
// Setup-Override (Events sind reine Diagnose-History).
|
||||
alertRetentionDays = 90
|
||||
|
||||
// backendDownCheckInterval — alle 2 Minuten HAProxy-Stats lesen und
|
||||
// prüfen ob ein Backend komplett ausgefallen ist (alle Server DOWN).
|
||||
// Dedupe 12h pro Backend → kein Alert-Spam. Frischer Alert wenn das
|
||||
@@ -185,7 +191,8 @@ func main() {
|
||||
auditRepo := audit.New(pool)
|
||||
alertDedupe := newDedupe(12 * time.Hour)
|
||||
|
||||
if renewer != nil {
|
||||
// ACME nur auf dem VIP-Master (siehe Tick-Kommentar unten).
|
||||
if renewer != nil && nodeHoldsVIP(ctx, pool) {
|
||||
runRenewer(ctx, renewer, alertSvc, alertDedupe)
|
||||
}
|
||||
runLicenseVerify(ctx, licClient, licKeyStore, licRepo, nodeID, alertSvc, alertDedupe)
|
||||
@@ -252,7 +259,13 @@ func main() {
|
||||
for {
|
||||
select {
|
||||
case <-renewTick.C:
|
||||
if renewer != nil {
|
||||
// ACME-HTTP-01-Challenges laufen auf :80 der VIP → nur der
|
||||
// VIP-Master kann sie bestehen. Ein BACKUP-Node scheitert IMMER
|
||||
// mit 403 (invalid authorization) und setzt tls_certs.status lokal
|
||||
// auf "error" → Divergenz zur replizierten Row (Primary=active) →
|
||||
// Config-Drift-Banner + Log-Noise. Renewal daher nur am VIP-Master;
|
||||
// die Cert-Row/PEM repliziert von dort ohnehin auf den Standby.
|
||||
if renewer != nil && nodeHoldsVIP(ctx, pool) {
|
||||
runRenewer(ctx, renewer, alertSvc, alertDedupe)
|
||||
}
|
||||
runCertExpiryCheck(ctx, tlsRepo, alertSvc, alertDedupe)
|
||||
@@ -272,6 +285,7 @@ func main() {
|
||||
runDiskCheck(ctx, alertSvc, alertDedupe)
|
||||
case <-auditTick.C:
|
||||
runAuditCleanup(ctx, auditRepo, setupStore)
|
||||
runAlertCleanup(ctx, alertSvc)
|
||||
case <-backendDownTick.C:
|
||||
runBackendDownCheck(ctx, pool, alertSvc, alertDedupe)
|
||||
case <-memTick.C:
|
||||
@@ -317,6 +331,29 @@ func runAuditCleanup(ctx context.Context, r *audit.Repo, setupStore *setup.Store
|
||||
}
|
||||
}
|
||||
|
||||
// runAlertCleanup löscht alert_events älter als alertRetentionDays.
|
||||
// Schutz vor unbounded growth — auf einer aktiven Box feuern backend.down/
|
||||
// mem.high/cert.expiring über Monate tausende Rows (die Tabelle ist
|
||||
// node-lokal, wird also nirgends sonst abgeräumt). Best-effort: Fehler
|
||||
// werden nur geloggt.
|
||||
func runAlertCleanup(ctx context.Context, a *alerts.Service) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
n, err := a.Cleanup(cctx, alertRetentionDays)
|
||||
if err != nil {
|
||||
slog.Warn("scheduler: alert cleanup failed",
|
||||
"keep_days", alertRetentionDays, "error", err)
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
slog.Info("scheduler: alert cleanup",
|
||||
"deleted", n, "keep_days", alertRetentionDays)
|
||||
}
|
||||
}
|
||||
|
||||
// runDiskCheck prüft die Belegung von / via statfs. Fire-Schwellen:
|
||||
// - >= 90% → Critical (error). Box ist akut gefährdet — beim
|
||||
// nächsten Backup-Run oder größeren apt-Update droht "no space
|
||||
@@ -592,7 +629,7 @@ func runWGClientTunnelCheck(ctx context.Context, pool *pgxpool.Pool, a *alerts.S
|
||||
// Alle aktiven Client-Interfaces aus DB laden.
|
||||
type wgIface struct{ name string }
|
||||
rows, err := pool.Query(ctx,
|
||||
`SELECT name FROM wg_interfaces WHERE mode = 'client' AND active = true ORDER BY name`)
|
||||
`SELECT name FROM wireguard_interfaces WHERE mode = 'client' AND active = true ORDER BY name`)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -663,6 +700,56 @@ func runWGClientTunnelCheck(ctx context.Context, pool *pgxpool.Pool, a *alerts.S
|
||||
|
||||
var egBackendRE = regexp.MustCompile(`^eg_backend_(\d+)$`)
|
||||
|
||||
// nodeHoldsVIP meldet true, wenn dieser Node aktuell mindestens eine
|
||||
// is_vip-Adresse lokal trägt — also der keepalived-MASTER ist. Nur der
|
||||
// Master hält die VLAN-Gateway-VIPs und erreicht damit die Backend-
|
||||
// Subnetze; ein BACKUP-Node hat KEINE VLAN-IP und sieht deshalb JEDES
|
||||
// Backend als L4-down. Spiegelt SystemHandler.VIPStatus (net.Interfaces,
|
||||
// kein Shell-out).
|
||||
func nodeHoldsVIP(ctx context.Context, pool *pgxpool.Pool) bool {
|
||||
if pool == nil {
|
||||
return false
|
||||
}
|
||||
rows, err := pool.Query(ctx,
|
||||
`SELECT address FROM ip_addresses WHERE is_vip = true AND active = true`)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer rows.Close()
|
||||
var vips []string
|
||||
for rows.Next() {
|
||||
var addr string
|
||||
if err := rows.Scan(&addr); err == nil {
|
||||
vips = append(vips, addr)
|
||||
}
|
||||
}
|
||||
if len(vips) == 0 {
|
||||
return false
|
||||
}
|
||||
local := make(map[string]bool)
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, ifc := range ifaces {
|
||||
addrs, err := ifc.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok {
|
||||
local[ipnet.IP.String()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, v := range vips {
|
||||
if local[v] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// runBackendDownCheck liest HAProxy-Stats via Admin-Socket und feuert
|
||||
// einen Error-Alert für jedes Backend bei dem alle Server DOWN sind
|
||||
// (und mind. einer einen echten Health-Check hat). Dedupe 12h pro Backend.
|
||||
@@ -670,6 +757,14 @@ func runBackendDownCheck(ctx context.Context, pool *pgxpool.Pool, a *alerts.Serv
|
||||
if a == nil || d == nil {
|
||||
return
|
||||
}
|
||||
// Nur auf dem VIP-Master prüfen. Ein BACKUP-Node hält die VLAN-
|
||||
// Gateway-VIPs nicht und kann die Backend-Subnetze gar nicht erreichen
|
||||
// → jeder Health-Check läuft L4TOUT → Dauer-"backend.down"-Fehlalarm
|
||||
// (Hauptquelle des alert_events-Spams). Der Master bedient den Traffic
|
||||
// und sieht die echten Backend-States.
|
||||
if !nodeHoldsVIP(ctx, pool) {
|
||||
return
|
||||
}
|
||||
dialer := net.Dialer{Timeout: 2 * time.Second}
|
||||
conn, err := dialer.DialContext(ctx, "unix", "/run/haproxy/admin.sock")
|
||||
if err != nil {
|
||||
|
||||
71
go.mod
71
go.mod
@@ -1,42 +1,41 @@
|
||||
module git.netcell-it.de/projekte/edgeguard-native
|
||||
|
||||
go 1.26.4
|
||||
go 1.27.1
|
||||
|
||||
require (
|
||||
github.com/corazawaf/coraza/v3 v3.7.0
|
||||
github.com/coreos/go-oidc/v3 v3.18.0
|
||||
github.com/dropmorepackets/haproxy-go v0.0.8
|
||||
github.com/coreos/go-oidc/v3 v3.21.0
|
||||
github.com/dropmorepackets/haproxy-go v0.1.1
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/go-acme/lego/v4 v4.35.2
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
github.com/minio/minio-go/v7 v7.1.0
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/jackc/pgx/v5 v5.11.0
|
||||
github.com/minio/minio-go/v7 v7.3.0
|
||||
github.com/pkg/sftp v1.13.11
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/pressly/goose/v3 v3.27.1
|
||||
github.com/pressly/goose/v3 v3.28.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/oauth2 v0.37.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/corazawaf/libinjection-go v0.3.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.23.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
@@ -48,43 +47,45 @@ require (
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/kaptinlin/go-i18n v0.1.4 // indirect
|
||||
github.com/kaptinlin/jsonschema v0.4.6 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/klauspost/compress v1.19.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magefile/mage v1.17.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/sethvargo/go-retry v0.4.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/tools v0.49.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
gopkg.in/ini.v1 v1.67.3 // indirect
|
||||
rsc.io/binaryregexp v0.2.0 // indirect
|
||||
)
|
||||
|
||||
176
go.sum
176
go.sum
@@ -1,33 +1,32 @@
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc=
|
||||
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc/go.mod h1:7rsocqNDkTCira5T0M7buoKR2ehh7YZiPkzxRuAgvVU=
|
||||
github.com/corazawaf/coraza/v3 v3.7.0 h1:LIQqu1r+l6e/U/gyiZeykWaNNBY1TzRLz+aaI+QYEEM=
|
||||
github.com/corazawaf/coraza/v3 v3.7.0/go.mod h1:dOSt5evqC7EstouEv6ghhui01+oVUwp9X1vybWwqTlo=
|
||||
github.com/corazawaf/libinjection-go v0.3.2 h1:9rrKt0lpg4WvUXt+lwS06GywfqRXXsa/7JcOw5cQLwI=
|
||||
github.com/corazawaf/libinjection-go v0.3.2/go.mod h1:Ik/+w3UmTWH9yn366RgS9D95K3y7Atb5m/H/gXzzPCk=
|
||||
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
|
||||
github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM=
|
||||
github.com/coreos/go-oidc/v3 v3.21.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dropmorepackets/haproxy-go v0.0.8 h1:kS2Wa8+ZDcnJdRSTiuBsaPun5hpdUPIuLQ+Drp9ZxYs=
|
||||
github.com/dropmorepackets/haproxy-go v0.0.8/go.mod h1:4a2AmmVjvg2zPNdizGZrMN8ZSUpj90U43VlcdbOIBnU=
|
||||
github.com/dropmorepackets/haproxy-go v0.1.1 h1:qYovzYpGHanQCBQW5k92uJqIdjcwSJQHZ0VFRiI3FJ0=
|
||||
github.com/dropmorepackets/haproxy-go v0.1.1/go.mod h1:4a2AmmVjvg2zPNdizGZrMN8ZSUpj90U43VlcdbOIBnU=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
|
||||
@@ -36,14 +35,12 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-acme/lego/v4 v4.35.2 h1:uVQg+KC/yj9R2g7Q9W5wDqhvQvxV5SMu5eqFVoN5xZU=
|
||||
github.com/go-acme/lego/v4 v4.35.2/go.mod h1:pX2jN5n8OphMGY1IaMjYm5DAEzguBaKRt8AvJAgJXpc=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -52,8 +49,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
@@ -73,8 +70,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
|
||||
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jcchavezs/mergefs v0.1.1 h1:D45R17m6dHnSVZefnhynoeZvcK2Uw0oTrRfoUOQ0S5Y=
|
||||
@@ -85,27 +82,21 @@ github.com/kaptinlin/go-i18n v0.1.4 h1:wCiwAn1LOcvymvWIVAM4m5dUAMiHunTdEubLDk4hT
|
||||
github.com/kaptinlin/go-i18n v0.1.4/go.mod h1:g1fn1GvTgT4CiLE8/fFE1hboHWJ6erivrDpiDtCcFKg=
|
||||
github.com/kaptinlin/jsonschema v0.4.6 h1:vOSFg5tjmfkOdKg+D6Oo4fVOM/pActWu/ntkPsI1T64=
|
||||
github.com/kaptinlin/jsonschema v0.4.6/go.mod h1:1DUd7r5SdyB2ZnMtyB7uLv64dE3zTFTiYytDCd+AEL0=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/magefile/mage v1.17.0 h1:dS4tkq997Ism03akafC8509iqDjeE7TNTexI25Y7sXM=
|
||||
github.com/magefile/mage v1.17.0/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
@@ -114,8 +105,8 @@ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8=
|
||||
github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA=
|
||||
github.com/minio/minio-go/v7 v7.3.0 h1:HM4pFCSQq/TK+j0/zmorSh5ddh81iDgRgU0BG0Vz/YU=
|
||||
github.com/minio/minio-go/v7 v7.3.0/go.mod h1:KUPWdecEO1LWyUz+sTGXAuf2jZHrPh5fCsRH86QbPfk=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -124,42 +115,44 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 h1:Vpr4VgAizEgEZsaMohpw6JYDP+i9Of9dmdY4ufNP6HI=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
||||
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4=
|
||||
github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM=
|
||||
github.com/pressly/goose/v3 v3.28.0 h1:D2M+iL31GmpZxSHOhX8mqyqAT3CXnokUmm0eKoSP+Vc=
|
||||
github.com/pressly/goose/v3 v3.28.0/go.mod h1:v26MOuB8bL3kzzrt3Vqhb3R0PRVsl8hFQKdrht/L6Rk=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/sethvargo/go-retry v0.4.0 h1:9qy1OoIAxBL+gBYnkTnTnWle5wlfsXQlwRzIbbpdqPw=
|
||||
github.com/sethvargo/go-retry v0.4.0/go.mod h1:tvsjdKG6xfiCx4LSiUZ06kcv38xvdVQwv8R6/VnnVWg=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -167,60 +160,61 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=
|
||||
github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw=
|
||||
gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0=
|
||||
modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY=
|
||||
modernc.org/libc v1.75.6 h1:yKk8qo+Di4gkmvRboK8ocCqH22FiUCR6jRy2OwtCRus=
|
||||
modernc.org/libc v1.75.6/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
|
||||
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||
modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg=
|
||||
modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
|
||||
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
107
internal/crowdsec/whitelist.go
Normal file
107
internal/crowdsec/whitelist.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package crowdsec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/configgen"
|
||||
)
|
||||
|
||||
// WhitelistPath ist die aus dem Domain-Flag crowdsec_trusted gerenderte
|
||||
// CrowdSec-Parser-Whitelist. s02-enrich läuft vor den Scenarios, sodass
|
||||
// whitelisted Events gar nicht erst in http-crawl-non_statics o. Ä. zählen.
|
||||
const WhitelistPath = "/etc/crowdsec/parsers/s02-enrich/edgeguard-admin-hosts-whitelist.yaml"
|
||||
|
||||
// WhitelistGenerator rendert eine host-genaue CrowdSec-Whitelist aus allen
|
||||
// Domains mit crowdsec_trusted=true. Vertrauenswürdige Admin-Panels (SPAs, die
|
||||
// pro Aktion viele /api/-Requests feuern) würden sonst das Scenario
|
||||
// http-crawl-non_statics auslösen und die Admin-IP bannen. No-op, wenn CrowdSec auf diesem Node nicht
|
||||
// installiert ist (managed-wenn-installiert).
|
||||
type WhitelistGenerator struct {
|
||||
pool *pgxpool.Pool
|
||||
SkipReload bool
|
||||
}
|
||||
|
||||
func NewWhitelistGenerator(pool *pgxpool.Pool) *WhitelistGenerator {
|
||||
return &WhitelistGenerator{pool: pool}
|
||||
}
|
||||
|
||||
func (g *WhitelistGenerator) Name() string { return "crowdsec-whitelist" }
|
||||
|
||||
func (g *WhitelistGenerator) Render(ctx context.Context) error {
|
||||
// Managed-wenn-installiert: ohne CrowdSec kein Whitelist-File.
|
||||
if !IsInstalled() {
|
||||
return nil
|
||||
}
|
||||
hosts, err := g.trustedHosts(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("crowdsec-whitelist: query: %w", err)
|
||||
}
|
||||
// Direktes Schreiben (kein tmp+rename): /etc/crowdsec/parsers/... ist
|
||||
// root-owned, edgeguard darf nur die eine (postinst-chownte) Datei
|
||||
// überschreiben — analog chrony/unbound.
|
||||
if err := os.WriteFile(WhitelistPath, renderWhitelist(hosts), 0o644); err != nil {
|
||||
return fmt.Errorf("crowdsec-whitelist: write %s: %w", WhitelistPath, err)
|
||||
}
|
||||
if g.SkipReload {
|
||||
return nil
|
||||
}
|
||||
return configgen.ReloadService("crowdsec")
|
||||
}
|
||||
|
||||
// RenderToString gibt die gerenderte Whitelist zurück (Config-Preview), ohne zu
|
||||
// schreiben oder zu reloaden.
|
||||
func (g *WhitelistGenerator) RenderToString(ctx context.Context) (string, error) {
|
||||
hosts, err := g.trustedHosts(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(renderWhitelist(hosts)), nil
|
||||
}
|
||||
|
||||
func (g *WhitelistGenerator) trustedHosts(ctx context.Context) ([]string, error) {
|
||||
rows, err := g.pool.Query(ctx,
|
||||
`SELECT name FROM domains WHERE crowdsec_trusted = true AND active = true ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var hosts []string
|
||||
for rows.Next() {
|
||||
var n string
|
||||
if err := rows.Scan(&n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hosts = append(hosts, n)
|
||||
}
|
||||
return hosts, rows.Err()
|
||||
}
|
||||
|
||||
// renderWhitelist baut die CrowdSec-Parser-Whitelist-YAML. Ohne vertrauens-
|
||||
// würdige Hosts bleibt die Ausdrucksliste leer → `in []` matcht nie → es wird
|
||||
// nichts whitelisted (Datei bleibt gültig). Pure Funktion (testbar).
|
||||
func renderWhitelist(hosts []string) []byte {
|
||||
quoted := make([]string, 0, len(hosts))
|
||||
for _, h := range hosts {
|
||||
h = strings.TrimSpace(h)
|
||||
if h == "" {
|
||||
continue
|
||||
}
|
||||
// Einfachquote + eingebettete Quotes verdoppeln (expr-String-Literal).
|
||||
quoted = append(quoted, "'"+strings.ReplaceAll(h, "'", "''")+"'")
|
||||
}
|
||||
var b bytes.Buffer
|
||||
b.WriteString("# Generated by edgeguard-api from domains.crowdsec_trusted. DO NOT EDIT.\n")
|
||||
b.WriteString("name: edgeguard/admin-hosts-whitelist\n")
|
||||
b.WriteString("description: Trusted admin panels (SPA fires many /api/ requests) exempted from CrowdSec - not a crawl.\n")
|
||||
b.WriteString("whitelist:\n")
|
||||
b.WriteString(" reason: edgeguard trusted admin host (SPA, not crawl/probing)\n")
|
||||
b.WriteString(" expression:\n")
|
||||
fmt.Fprintf(&b, " - \"evt.Parsed.http_host in [%s]\"\n", strings.Join(quoted, ", "))
|
||||
return b.Bytes()
|
||||
}
|
||||
35
internal/crowdsec/whitelist_test.go
Normal file
35
internal/crowdsec/whitelist_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package crowdsec
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRenderWhitelist(t *testing.T) {
|
||||
t.Run("hosts werden host-genau eingetragen", func(t *testing.T) {
|
||||
out := string(renderWhitelist([]string{"control.netcell-it.de", "admin.example.com"}))
|
||||
if !strings.Contains(out, "evt.Parsed.http_host in ['control.netcell-it.de', 'admin.example.com']") {
|
||||
t.Fatalf("erwartete host-Liste fehlt:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "name: edgeguard/admin-hosts-whitelist") {
|
||||
t.Fatalf("Parser-Name fehlt:\n%s", out)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("leere Liste → in [] (matcht nie, Datei gültig)", func(t *testing.T) {
|
||||
out := string(renderWhitelist(nil))
|
||||
if !strings.Contains(out, "evt.Parsed.http_host in []") {
|
||||
t.Fatalf("erwarte leeres in []:\n%s", out)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("leere/whitespace-Hosts werden gefiltert, Quotes escaped", func(t *testing.T) {
|
||||
out := string(renderWhitelist([]string{" ", "a'b.de", ""}))
|
||||
if !strings.Contains(out, "'a''b.de'") {
|
||||
t.Fatalf("Quote-Escaping falsch:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "'', ") || strings.Contains(out, "[''") {
|
||||
t.Fatalf("leere Hosts nicht gefiltert:\n%s", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
27
internal/database/migrations/0044_backend_server_timeout.sql
Normal file
27
internal/database/migrations/0044_backend_server_timeout.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- Per-Backend `timeout server` (Sekunden). NULL = defaults-Timeout (60s,
|
||||
-- siehe haproxy.cfg.tpl). Gedacht für Upstreams die LANGE für die Antwort
|
||||
-- brauchen und dabei NICHT streamen — z. B. KI-/Inferenz-Server, die eine
|
||||
-- gepufferte Antwort erst nach Minuten schicken. Ohne Override kappt der
|
||||
-- 60s-defaults-Timeout diese Requests.
|
||||
--
|
||||
-- Bewusst NULL-per-default: Backends ohne Langläufer-Workload behalten den
|
||||
-- kurzen Timeout (Connection-Hygiene / Slowloris-Schutz, vgl. v1.3.2).
|
||||
-- Der Renderer setzt `timeout server <N>s` NUR wenn ein Wert gesetzt ist.
|
||||
--
|
||||
-- CHECK 1..86400: mind. 1s, max. 24h — verhindert 0/negativ (würde HAProxy-
|
||||
-- Config sprengen bzw. „unendlich" bedeuten) und absurd hohe Werte.
|
||||
ALTER TABLE backends
|
||||
ADD COLUMN IF NOT EXISTS server_timeout_seconds INTEGER
|
||||
CONSTRAINT backends_server_timeout_range
|
||||
CHECK (server_timeout_seconds IS NULL
|
||||
OR (server_timeout_seconds BETWEEN 1 AND 86400));
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE backends DROP COLUMN IF EXISTS server_timeout_seconds;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,22 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- Quittieren von Alarmen. acknowledged_at = NULL → offen (zählt im Dashboard).
|
||||
-- Gesetzt → quittiert (bleibt als History sichtbar, zählt aber nicht mehr auf
|
||||
-- der Startseiten-Karte "Aktuelle Alerts"). alert_events ist node-lokal
|
||||
-- (localOnlyTables) → kein Replikations-Effekt.
|
||||
ALTER TABLE alert_events
|
||||
ADD COLUMN IF NOT EXISTS acknowledged_at TIMESTAMPTZ;
|
||||
|
||||
-- Teil-Index für den Dashboard-Query (nur offene, newest-first).
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_events_open
|
||||
ON alert_events (fired_at DESC)
|
||||
WHERE acknowledged_at IS NULL;
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP INDEX IF EXISTS idx_alert_events_open;
|
||||
ALTER TABLE alert_events DROP COLUMN IF EXISTS acknowledged_at;
|
||||
-- +goose StatementEnd
|
||||
19
internal/database/migrations/0046_waf_crs_plugins.sql
Normal file
19
internal/database/migrations/0046_waf_crs_plugins.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- CRS-App-Exclusion-Plugins pro Domain (OWASP-CRS-Plugin-System). Liste von
|
||||
-- Plugin-Namen (z. B. 'nextcloud','wordpress','drupal'). Der WAF-Renderer
|
||||
-- inkludiert je gewähltem Plugin dessen config/before/after-Dateien aus
|
||||
-- <crsDir>/plugins/ an den korrekten Punkten (config+before VOR den CRS-Rules,
|
||||
-- after DANACH) → pfad-genaue, upstream-gepflegte App-Ausnahmen statt manueller
|
||||
-- SecRuleRemoveById-IDs. waf_configs ist repliziert; der Renderer läuft pro
|
||||
-- Node lokal, daher kein Cross-Node-Effekt außer der Config selbst.
|
||||
ALTER TABLE waf_configs
|
||||
ADD COLUMN IF NOT EXISTS crs_plugins TEXT[] NOT NULL DEFAULT '{}';
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE waf_configs DROP COLUMN IF EXISTS crs_plugins;
|
||||
-- +goose StatementEnd
|
||||
34
internal/database/migrations/0047_waf_app_profiles.sql
Normal file
34
internal/database/migrations/0047_waf_app_profiles.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- Benutzerdefinierte WAF-App-Profile: benannte, wiederverwendbare Bündel von
|
||||
-- CRS-Rule-Exclusions (reine Rule-IDs/Ranges — keine SecLang-Ausführung, sicher).
|
||||
-- Wirken wie die eingebauten OWASP-Plugins, sind aber im UI erstellbar/editierbar
|
||||
-- und werden pro Domain zugewiesen (waf_configs.app_profiles). Die Auflösung in
|
||||
-- effektive SecRuleRemoveById-Zeilen passiert im WAF-Agent (ListAllWithDomain).
|
||||
--
|
||||
-- Repliziert (Config, kein node-lokaler Zustand) → vom cluster-reconcile
|
||||
-- automatisch in edgeguard_shared aufgenommen (nicht in localOnlyTables).
|
||||
CREATE TABLE IF NOT EXISTS waf_app_profiles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
rule_exclusions TEXT[] NOT NULL DEFAULT '{}',
|
||||
builtin BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Zuweisung Profil→Domain: Liste von Profil-Namen je waf_config. Beim Bauen der
|
||||
-- Engine werden ihre rule_exclusions in die effektiven Ausnahmen der Domain
|
||||
-- gemischt (zusätzlich zu den domain-eigenen rule_exclusions).
|
||||
ALTER TABLE waf_configs
|
||||
ADD COLUMN IF NOT EXISTS app_profiles TEXT[] NOT NULL DEFAULT '{}';
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE waf_configs DROP COLUMN IF EXISTS app_profiles;
|
||||
DROP TABLE IF EXISTS waf_app_profiles;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,18 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- Pro-Domain-Flag: vertrauenswuerdiges Admin-Panel → von CrowdSec ausnehmen.
|
||||
-- Admin-SPAs feuern viele /api/-Requests pro Aktion und triggern sonst das
|
||||
-- Scenario http-crawl-non_statics (False-Positive-Ban der Admin-IP, die oft
|
||||
-- dynamisch ist). Der crowdsec-Whitelist-Renderer schreibt aus allen Domains
|
||||
-- mit crowdsec_trusted=true eine host-genaue CrowdSec-Parser-Whitelist
|
||||
-- (evt.Parsed.http_host). Repliziert (Config, node-lokal gerendert) → ueberlebt
|
||||
-- auch einen Node-Neuaufbau, weil aus der DB gerendert.
|
||||
ALTER TABLE domains ADD COLUMN IF NOT EXISTS crowdsec_trusted BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE domains DROP COLUMN IF EXISTS crowdsec_trusted;
|
||||
-- +goose StatementEnd
|
||||
@@ -18,7 +18,10 @@ import (
|
||||
// PUT /api/v1/alerts/channels/:id
|
||||
// DELETE /api/v1/alerts/channels/:id
|
||||
// POST /api/v1/alerts/test — Test-Event in alle aktiven Channels
|
||||
// GET /api/v1/alerts/events?limit=N — History
|
||||
// GET /api/v1/alerts/events?limit=N&open=true — History (open=nur offene)
|
||||
// POST /api/v1/alerts/events/acknowledge — Bulk-Quittieren {ids:[…]}
|
||||
// POST /api/v1/alerts/events/acknowledge-all — alle offenen quittieren
|
||||
// POST /api/v1/alerts/events/delete — Bulk-Löschen {ids:[…]}
|
||||
type AlertsHandler struct {
|
||||
Service *alerts.Service
|
||||
Audit *audit.Repo
|
||||
@@ -37,6 +40,9 @@ func (h *AlertsHandler) Register(rg *gin.RouterGroup) {
|
||||
g.DELETE("/channels/:id", h.DeleteChannel)
|
||||
g.POST("/test", h.TestFire)
|
||||
g.GET("/events", h.ListEvents)
|
||||
g.POST("/events/acknowledge", h.AcknowledgeEvents)
|
||||
g.POST("/events/acknowledge-all", h.AcknowledgeAllEvents)
|
||||
g.POST("/events/delete", h.DeleteEvents)
|
||||
}
|
||||
|
||||
func (h *AlertsHandler) ListChannels(c *gin.Context) {
|
||||
@@ -125,10 +131,64 @@ func (h *AlertsHandler) ListEvents(c *gin.Context) {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
out, err := h.Service.ListEvents(c.Request.Context(), limit)
|
||||
// ?open=true → nur offene (nicht quittierte) Events. Nutzt die
|
||||
// Dashboard-Karte, damit Quittieren die Meldung verschwinden lässt.
|
||||
openOnly := c.Query("open") == "true"
|
||||
out, err := h.Service.ListEvents(c.Request.Context(), limit, openOnly)
|
||||
if err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"events": out})
|
||||
}
|
||||
|
||||
// eventIDsRequest ist der Body für Bulk-Quittieren/-Löschen.
|
||||
type eventIDsRequest struct {
|
||||
IDs []int64 `json:"ids"`
|
||||
}
|
||||
|
||||
// AcknowledgeEvents quittiert die übergebenen Event-IDs.
|
||||
func (h *AlertsHandler) AcknowledgeEvents(c *gin.Context) {
|
||||
var req eventIDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
n, err := h.Service.Acknowledge(c.Request.Context(), req.IDs)
|
||||
if err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "alert.events.acknowledge",
|
||||
strconv.Itoa(len(req.IDs)), gin.H{"ids": req.IDs, "acknowledged": n}, h.NodeID)
|
||||
response.OK(c, gin.H{"acknowledged": n})
|
||||
}
|
||||
|
||||
// AcknowledgeAllEvents quittiert alle offenen Events.
|
||||
func (h *AlertsHandler) AcknowledgeAllEvents(c *gin.Context) {
|
||||
n, err := h.Service.AcknowledgeAll(c.Request.Context())
|
||||
if err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "alert.events.acknowledge_all",
|
||||
"all", gin.H{"acknowledged": n}, h.NodeID)
|
||||
response.OK(c, gin.H{"acknowledged": n})
|
||||
}
|
||||
|
||||
// DeleteEvents löscht die übergebenen Event-IDs endgültig.
|
||||
func (h *AlertsHandler) DeleteEvents(c *gin.Context) {
|
||||
var req eventIDsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
n, err := h.Service.DeleteEvents(c.Request.Context(), req.IDs)
|
||||
if err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "alert.events.delete",
|
||||
strconv.Itoa(len(req.IDs)), gin.H{"ids": req.IDs, "deleted": n}, h.NodeID)
|
||||
response.OK(c, gin.H{"deleted": n})
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/cluster/jointoken"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||
aptsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/apt"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
|
||||
)
|
||||
|
||||
@@ -92,6 +93,8 @@ func (h *ClusterHandler) Register(rg *gin.RouterGroup) {
|
||||
g.GET("/repair-replication/status", h.RepairReplicationStatus)
|
||||
g.GET("/vip-status", h.VIPStatus)
|
||||
g.POST("/vip-test", h.VIPTest)
|
||||
g.GET("/update-channel", h.UpdateChannel)
|
||||
g.POST("/update-channel", h.SetUpdateChannel)
|
||||
if h.TLSStore != nil {
|
||||
g.GET("/cert-status", h.CertStatus)
|
||||
g.POST("/renew-self", h.RenewSelf)
|
||||
@@ -247,6 +250,8 @@ func (h *ClusterHandler) RegisterAgent(rg *gin.RouterGroup) {
|
||||
g.GET("/master-key", h.AgentMasterKey)
|
||||
g.GET("/version", h.AgentVersion)
|
||||
g.POST("/trigger-update", h.AgentTriggerUpdate)
|
||||
g.POST("/set-channel", h.AgentSetChannel)
|
||||
g.GET("/channel", h.AgentChannel)
|
||||
g.GET("/active-ips", h.AgentActiveIPs)
|
||||
g.POST("/vip-cmd", h.AgentVIPCmd)
|
||||
g.GET("/tls-certs", h.AgentTLSCerts)
|
||||
@@ -758,7 +763,10 @@ retry_apt() {
|
||||
while [ $attempt -lt $max ]; do
|
||||
attempt=$((attempt + 1))
|
||||
apt-get update -qq || true
|
||||
if apt-get install -y -qq -o Dpkg::Options::=--force-confold \
|
||||
# --allow-downgrades: nur relevant nach testing→stable-Kanalwechsel
|
||||
# (Testing-Versionen sortieren datumsbasiert höher als Stable-Semver).
|
||||
# No-Op im Normalfall, da die Candidate sonst immer >= installed ist.
|
||||
if apt-get install -y -qq --allow-downgrades -o Dpkg::Options::=--force-confold \
|
||||
edgeguard-api edgeguard-ui edgeguard; then return 0; fi
|
||||
[ $attempt -lt $max ] && sleep $wait_for && wait_for=$((wait_for * 2))
|
||||
done
|
||||
@@ -789,6 +797,131 @@ rm -f /var/lib/edgeguard/upgrade.sh
|
||||
c.JSON(http.StatusAccepted, gin.H{"status": "upgrading"})
|
||||
}
|
||||
|
||||
// ── Update-Kanal (stable/testing) ──────────────────────────────────────
|
||||
//
|
||||
// Kanal-Modell wie enconf (Suite=Codename, Komponente=Kanal, siehe
|
||||
// internal/services/apt.Channel/SetChannel) — an EdgeGuards fixes
|
||||
// Primary/Standby-Paar angepasst statt generischer Server-Flotte: der
|
||||
// Kanal wird auf beiden Nodes synchron gehalten (wie config_hash),
|
||||
// kein Node-Override. Reines Umschreiben der sources.list + `apt-get
|
||||
// update` ist risikofrei (kein Service-Restart, keine VIP-Auswirkung)
|
||||
// — das eigentliche Downgrade/Upgrade auf die neue Kanal-Version läuft
|
||||
// danach ganz normal über den bestehenden (sicheren, Standby-zuerst)
|
||||
// Rolling-Update-Flow, der --allow-downgrades jetzt mit unterstützt.
|
||||
|
||||
type updateChannelResponse struct {
|
||||
Channel string `json:"channel"`
|
||||
PeerChannel string `json:"peer_channel,omitempty"`
|
||||
PeerReached bool `json:"peer_reached"`
|
||||
PeerDrifted bool `json:"peer_drifted"`
|
||||
}
|
||||
|
||||
// UpdateChannel liefert den lokalen Kanal + (falls Cluster) den Kanal
|
||||
// des Peers zur Drift-Erkennung — analog zum config_hash-Vergleich.
|
||||
func (h *ClusterHandler) UpdateChannel(c *gin.Context) {
|
||||
resp := updateChannelResponse{Channel: aptsvc.Channel()}
|
||||
peer := h.peerNode(c.Request.Context())
|
||||
if peer != nil && h.Aggregator != nil {
|
||||
results := h.Aggregator.FanOut(c.Request.Context(), []models.HANode{*peer}, "/agent/cluster/channel", h.LocalID)
|
||||
if len(results) > 0 && results[0].OK {
|
||||
var body struct {
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
if json.Unmarshal(results[0].Data, &body) == nil {
|
||||
resp.PeerReached = true
|
||||
resp.PeerChannel = body.Channel
|
||||
resp.PeerDrifted = body.Channel != resp.Channel
|
||||
}
|
||||
}
|
||||
}
|
||||
response.OK(c, resp)
|
||||
}
|
||||
|
||||
// SetUpdateChannel setzt den Kanal lokal und — falls ein Peer existiert
|
||||
// — synchron auch auf dem Peer via mTLS. Löst KEIN Paket-Update aus;
|
||||
// das übernimmt der Admin danach ganz normal über den Update-Banner /
|
||||
// Rolling-Update, der die neue Candidate-Version dann bereits sieht.
|
||||
func (h *ClusterHandler) SetUpdateChannel(c *gin.Context) {
|
||||
var req struct {
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if req.Channel != "stable" && req.Channel != "testing" {
|
||||
response.BadRequest(c, fmt.Errorf("channel must be 'stable' or 'testing'"))
|
||||
return
|
||||
}
|
||||
if err := aptsvc.SetChannel(c.Request.Context(), req.Channel); err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := updateChannelResponse{Channel: req.Channel}
|
||||
if peer := h.peerNode(c.Request.Context()); peer != nil && h.Aggregator != nil {
|
||||
body, _ := json.Marshal(req)
|
||||
result := h.Aggregator.PostPeerWithBody(c.Request.Context(), *peer, "/agent/cluster/set-channel", body)
|
||||
resp.PeerReached = result.OK
|
||||
if !result.OK {
|
||||
slog.Warn("cluster: set-channel on peer failed", "peer", peer.FQDN, "error", result.Err)
|
||||
}
|
||||
}
|
||||
|
||||
if h.Audit != nil {
|
||||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "system.update_channel.set",
|
||||
"", gin.H{"channel": req.Channel}, h.NodeID)
|
||||
}
|
||||
response.OK(c, resp)
|
||||
}
|
||||
|
||||
// AgentChannel: mTLS-Peer-Read des lokalen Kanals (für Drift-Anzeige).
|
||||
func (h *ClusterHandler) AgentChannel(c *gin.Context) {
|
||||
response.OK(c, gin.H{"channel": aptsvc.Channel()})
|
||||
}
|
||||
|
||||
// AgentSetChannel: mTLS-Peer-Write — wird vom Primary aufgerufen um den
|
||||
// Kanal auf diesem (Standby-)Node synchron zu setzen.
|
||||
func (h *ClusterHandler) AgentSetChannel(c *gin.Context) {
|
||||
var req struct {
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if req.Channel != "stable" && req.Channel != "testing" {
|
||||
response.BadRequest(c, fmt.Errorf("channel must be 'stable' or 'testing'"))
|
||||
return
|
||||
}
|
||||
if err := aptsvc.SetChannel(c.Request.Context(), req.Channel); err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
slog.Info("cluster: update channel set on this node by primary mTLS call",
|
||||
"channel", req.Channel, "client", c.ClientIP())
|
||||
response.OK(c, gin.H{"channel": req.Channel})
|
||||
}
|
||||
|
||||
// peerNode liefert die einzige andere ha_nodes-Row (best-effort, nil
|
||||
// wenn Standalone oder Store fehlt) — gleiches Muster wie in
|
||||
// RollingUpdate für die Secondary-Ermittlung.
|
||||
func (h *ClusterHandler) peerNode(ctx context.Context) *models.HANode {
|
||||
if h.Store == nil {
|
||||
return nil
|
||||
}
|
||||
nodes, err := h.Store.List(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for i := range nodes {
|
||||
if nodes[i].ID != h.LocalID {
|
||||
return &nodes[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var errInvalidJoinRequest = simpleError("missing token or csr")
|
||||
|
||||
type simpleError string
|
||||
|
||||
@@ -109,6 +109,40 @@ func SyncTLSCertsFromPrimary(ctx context.Context, pool *pgxpool.Pool, agg *aggre
|
||||
slog.Info("cert-sync: updated", "file", name)
|
||||
}
|
||||
|
||||
// Prune: lokale .pem entfernen, die der Primary NICHT (mehr) hat.
|
||||
// Ohne diesen Schritt bleiben Zertifikate gelöschter Domains auf dem
|
||||
// Secondary als Waisen liegen — der Sync oben ist write-only, „nicht
|
||||
// mitgeschickt" ≠ „gelöscht". Geschützt bleiben:
|
||||
// _default.pem — Self-Signed-Fallback
|
||||
// <lokaler-FQDN>.pem — eigener Node-Cert (steht NICHT im Primary-Payload)
|
||||
// Nur prunen wenn der Payload nicht leer ist — Schutz gegen ein
|
||||
// versehentliches Leerräumen bei unvollständiger Primary-Antwort.
|
||||
if len(payload.Certs) > 0 {
|
||||
protected := map[string]bool{"_default.pem": true}
|
||||
var localFQDN string
|
||||
if err := pool.QueryRow(ctx,
|
||||
`SELECT fqdn FROM ha_nodes WHERE id = $1`, localID).Scan(&localFQDN); err == nil && localFQDN != "" {
|
||||
protected[localFQDN+".pem"] = true
|
||||
}
|
||||
if entries, err := os.ReadDir(tlsCertDir); err == nil {
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if e.IsDir() || !strings.HasSuffix(name, ".pem") || protected[name] {
|
||||
continue
|
||||
}
|
||||
if _, ok := payload.Certs[name]; ok {
|
||||
continue // vom Primary gepflegt — behalten
|
||||
}
|
||||
if err := os.Remove(filepath.Join(tlsCertDir, name)); err != nil {
|
||||
slog.Warn("cert-sync: prune failed", "file", name, "error", err)
|
||||
continue
|
||||
}
|
||||
changed = true
|
||||
slog.Info("cert-sync: pruned orphan", "file", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
if err := exec.Command("sudo", "-n", "/usr/bin/systemctl", "reload", "haproxy.service").Run(); err != nil {
|
||||
slog.Warn("cert-sync: haproxy reload failed", "error", err)
|
||||
|
||||
@@ -273,7 +273,10 @@ retry_apt() {
|
||||
while [ $attempt -lt $max ]; do
|
||||
attempt=$((attempt + 1))
|
||||
apt-get update -qq || true
|
||||
if apt-get install -y -qq -o Dpkg::Options::=--force-confold \
|
||||
# --allow-downgrades: nur relevant nach testing→stable-Kanalwechsel
|
||||
# (Testing-Versionen sortieren datumsbasiert höher als Stable-Semver).
|
||||
# No-Op im Normalfall, da die Candidate sonst immer >= installed ist.
|
||||
if apt-get install -y -qq --allow-downgrades -o Dpkg::Options::=--force-confold \
|
||||
edgeguard-api edgeguard-ui edgeguard; then return 0; fi
|
||||
[ $attempt -lt $max ] && sleep $wait_for && wait_for=$((wait_for * 2))
|
||||
done
|
||||
|
||||
@@ -940,7 +940,10 @@ retry_apt() {
|
||||
attempt=$((attempt + 1))
|
||||
echo "[upgrade] attempt $attempt/$max: apt-get update + install"
|
||||
apt-get update -qq || true
|
||||
if apt-get install -y -qq -o Dpkg::Options::=--force-confold \
|
||||
# --allow-downgrades: nur relevant nach testing→stable-Kanalwechsel
|
||||
# (Testing-Versionen sortieren datumsbasiert höher als Stable-Semver).
|
||||
# No-Op im Normalfall, da die Candidate sonst immer >= installed ist.
|
||||
if apt-get install -y -qq --allow-downgrades -o Dpkg::Options::=--force-confold \
|
||||
edgeguard-api edgeguard-ui edgeguard; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
|
||||
wafsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/waf"
|
||||
intwaf "git.netcell-it.de/projekte/edgeguard-native/internal/waf"
|
||||
)
|
||||
|
||||
// wafRuleIDRe erlaubt nur einzelne CRS-Rule-IDs oder Ranges ("942100" /
|
||||
@@ -45,6 +46,11 @@ func (h *WafHandler) Register(rg *gin.RouterGroup) {
|
||||
g.PUT("/configs/:domain_id", h.Upsert)
|
||||
g.GET("/alerts", h.ListAlerts)
|
||||
g.DELETE("/alerts", h.PurgeAlerts)
|
||||
// Benutzerdefinierte App-Profile (wiederverwendbare Rule-ID-Bündel).
|
||||
g.GET("/profiles", h.ListProfiles)
|
||||
g.POST("/profiles", h.CreateProfile)
|
||||
g.PUT("/profiles/:id", h.UpdateProfile)
|
||||
g.DELETE("/profiles/:id", h.DeleteProfile)
|
||||
}
|
||||
|
||||
// List returns all WAF configs.
|
||||
@@ -80,13 +86,15 @@ func (h *WafHandler) Get(c *gin.Context) {
|
||||
|
||||
// upsertBody is the accepted JSON for PUT /waf/configs/:domain_id.
|
||||
type upsertBody struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Mode string `json:"mode"`
|
||||
ParanoiaLevel int `json:"paranoia_level"`
|
||||
RuleExclusions []string `json:"rule_exclusions"`
|
||||
ExclusionNotes map[string]string `json:"exclusion_notes"`
|
||||
TrustedProxies []string `json:"trusted_proxies"`
|
||||
CustomRules string `json:"custom_rules"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Mode string `json:"mode"`
|
||||
ParanoiaLevel int `json:"paranoia_level"`
|
||||
RuleExclusions []string `json:"rule_exclusions"`
|
||||
CRSPlugins []string `json:"crs_plugins"`
|
||||
AppProfiles []string `json:"app_profiles"`
|
||||
ExclusionNotes map[string]string `json:"exclusion_notes"`
|
||||
TrustedProxies []string `json:"trusted_proxies"`
|
||||
CustomRules string `json:"custom_rules"`
|
||||
}
|
||||
|
||||
// Upsert creates or updates the WAF config for a domain.
|
||||
@@ -110,9 +118,32 @@ func (h *WafHandler) Upsert(c *gin.Context) {
|
||||
if body.RuleExclusions == nil {
|
||||
body.RuleExclusions = []string{}
|
||||
}
|
||||
if body.CRSPlugins == nil {
|
||||
body.CRSPlugins = []string{}
|
||||
}
|
||||
if body.TrustedProxies == nil {
|
||||
body.TrustedProxies = []string{}
|
||||
}
|
||||
if body.AppProfiles == nil {
|
||||
body.AppProfiles = []string{}
|
||||
}
|
||||
// App-Profile: nur trimmen/leere raus. Unbekannte Namen sind harmlos (der
|
||||
// Agent-Resolver ignoriert sie defensiv), aber wir speichern keinen Müll.
|
||||
cleanProfiles := make([]string, 0, len(body.AppProfiles))
|
||||
for _, p := range body.AppProfiles {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
cleanProfiles = append(cleanProfiles, p)
|
||||
}
|
||||
}
|
||||
body.AppProfiles = cleanProfiles
|
||||
// CRS-Plugins müssen aus der bekannten Whitelist stammen — sie werden zu
|
||||
// Include-Pfaden, ein unbekannter Name wäre Pfad-Injection.
|
||||
for _, p := range body.CRSPlugins {
|
||||
if _, ok := intwaf.KnownCRSPlugins[strings.TrimSpace(p)]; !ok {
|
||||
response.BadRequest(c, errors.New("unbekanntes CRS-Plugin: "+p))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if body.ExclusionNotes == nil {
|
||||
body.ExclusionNotes = map[string]string{}
|
||||
@@ -144,6 +175,8 @@ func (h *WafHandler) Upsert(c *gin.Context) {
|
||||
Mode: body.Mode,
|
||||
ParanoiaLevel: body.ParanoiaLevel,
|
||||
RuleExclusions: body.RuleExclusions,
|
||||
CRSPlugins: body.CRSPlugins,
|
||||
AppProfiles: body.AppProfiles,
|
||||
ExclusionNotes: body.ExclusionNotes,
|
||||
TrustedProxies: body.TrustedProxies,
|
||||
CustomRules: body.CustomRules,
|
||||
@@ -211,6 +244,119 @@ func (h *WafHandler) PurgeAlerts(c *gin.Context) {
|
||||
response.OK(c, gin.H{"ok": true, "days": days})
|
||||
}
|
||||
|
||||
// wafProfileNameRe: erlaubte Zeichen für App-Profil-Namen (der Name wird pro
|
||||
// Domain in waf_configs.app_profiles referenziert; kein SecLang-Kontext, aber
|
||||
// sauber begrenzen).
|
||||
var wafProfileNameRe = regexp.MustCompile(`^[A-Za-z0-9 ._-]{1,60}$`)
|
||||
|
||||
// profileBody ist das akzeptierte JSON für Create/Update eines App-Profils.
|
||||
type profileBody struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
RuleExclusions []string `json:"rule_exclusions"`
|
||||
}
|
||||
|
||||
// validateProfileBody normalisiert und prüft den Request-Body. Gibt eine
|
||||
// Fehlermeldung zurück (nil = ok) und mutiert body (trim, nil→[]).
|
||||
func validateProfileBody(body *profileBody) error {
|
||||
body.Name = strings.TrimSpace(body.Name)
|
||||
if !wafProfileNameRe.MatchString(body.Name) {
|
||||
return errors.New("ungültiger Profil-Name (1–60 Zeichen: Buchstaben, Ziffern, Leer, . _ -)")
|
||||
}
|
||||
body.Description = strings.TrimSpace(body.Description)
|
||||
if body.RuleExclusions == nil {
|
||||
body.RuleExclusions = []string{}
|
||||
}
|
||||
for i, ex := range body.RuleExclusions {
|
||||
ex = strings.TrimSpace(ex)
|
||||
if !wafRuleIDRe.MatchString(ex) {
|
||||
return errors.New("ungültige Rule-Exclusion (nur IDs/Ranges erlaubt): " + ex)
|
||||
}
|
||||
body.RuleExclusions[i] = ex
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListProfiles returns all WAF app profiles (built-in first).
|
||||
func (h *WafHandler) ListProfiles(c *gin.Context) {
|
||||
profiles, err := h.Repo.ListProfiles(c.Request.Context())
|
||||
if err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"profiles": profiles})
|
||||
}
|
||||
|
||||
// CreateProfile creates a new user-defined app profile.
|
||||
func (h *WafHandler) CreateProfile(c *gin.Context) {
|
||||
var body profileBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := validateProfileBody(&body); err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
p, err := h.Repo.CreateProfile(c.Request.Context(), body.Name, body.Description, body.RuleExclusions)
|
||||
if err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "waf.profile.create", body.Name,
|
||||
gin.H{"exclusions": len(body.RuleExclusions)}, h.NodeID)
|
||||
c.JSON(http.StatusOK, gin.H{"profile": p})
|
||||
}
|
||||
|
||||
// UpdateProfile updates a user-defined app profile (built-ins are read-only).
|
||||
func (h *WafHandler) UpdateProfile(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, errors.New("invalid id"))
|
||||
return
|
||||
}
|
||||
var body profileBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := validateProfileBody(&body); err != nil {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
p, err := h.Repo.UpdateProfile(c.Request.Context(), id, body.Name, body.Description, body.RuleExclusions)
|
||||
if err != nil {
|
||||
if errors.Is(err, wafsvc.ErrProfileNotFound) {
|
||||
response.BadRequest(c, errors.New("kein Profil gefunden oder read-only (built-in)"))
|
||||
return
|
||||
}
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "waf.profile.update", body.Name,
|
||||
gin.H{"exclusions": len(body.RuleExclusions)}, h.NodeID)
|
||||
c.JSON(http.StatusOK, gin.H{"profile": p})
|
||||
}
|
||||
|
||||
// DeleteProfile removes a user-defined app profile (built-ins are protected).
|
||||
func (h *WafHandler) DeleteProfile(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, errors.New("invalid id"))
|
||||
return
|
||||
}
|
||||
if err := h.Repo.DeleteProfile(c.Request.Context(), id); err != nil {
|
||||
if errors.Is(err, wafsvc.ErrProfileNotFound) {
|
||||
response.BadRequest(c, errors.New("kein Profil gefunden oder read-only (built-in)"))
|
||||
return
|
||||
}
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "waf.profile.delete", strconv.FormatInt(id, 10), nil, h.NodeID)
|
||||
response.OK(c, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// defaultConfig returns a sensible disabled default for a domain
|
||||
// that has no WAF config row yet.
|
||||
func defaultConfig(domainID int64) models.WafConfig {
|
||||
@@ -220,6 +366,8 @@ func defaultConfig(domainID int64) models.WafConfig {
|
||||
Mode: "detection",
|
||||
ParanoiaLevel: 1,
|
||||
RuleExclusions: []string{},
|
||||
CRSPlugins: []string{},
|
||||
AppProfiles: []string{},
|
||||
ExclusionNotes: map[string]string{},
|
||||
TrustedProxies: []string{},
|
||||
CustomRules: "",
|
||||
|
||||
@@ -81,6 +81,13 @@ frontend public_https
|
||||
bind quic6@:443 ssl crt /etc/edgeguard/tls/ alpn h3
|
||||
{{- end}}
|
||||
{{- if .WAFEnabled}}
|
||||
# WAF: Request-Body puffern, damit edgeguard-waf den Body inspizieren
|
||||
# kann (POST/PUT-Payloads: Form-SQLi, JSON-Injection, Datei-Uploads).
|
||||
# Bewusst NUR wenn mind. eine Domain WAF nutzt (.WAFEnabled) — sonst
|
||||
# kein RAM-pro-Connection-Overhead (vgl. Kommentar am Body-Size-Cap).
|
||||
# Puffer bis tune.bufsize (~16KB); größere Bodies werden zur Inspektion
|
||||
# gekappt — typische Injection-Payloads sind klein.
|
||||
option http-buffer-request
|
||||
# WAF: SPOE-Filter — edgeguard-waf inspiziert jeden Request.
|
||||
# filter muss vor allen http-request/http-response-Direktiven stehen.
|
||||
filter spoe engine edgeguard-waf config /etc/edgeguard/haproxy/coraza-spoe.cfg
|
||||
@@ -226,6 +233,11 @@ backend eg_backend_{{$b.ID}}
|
||||
{{- if $b.WebSocket}}
|
||||
timeout tunnel 1h
|
||||
{{- end}}
|
||||
{{- if $b.ServerTimeoutSeconds}}
|
||||
# Override des defaults-`timeout server 60s` für langsame Upstreams
|
||||
# (z. B. KI-Server mit gepufferter Antwort). Wert per Backend gepflegt.
|
||||
timeout server {{$b.ServerTimeoutSeconds}}s
|
||||
{{- end}}
|
||||
{{- if $b.HealthCheckPath}}
|
||||
option httpchk
|
||||
http-check send meth GET uri {{$b.HealthCheckPath}}
|
||||
|
||||
@@ -160,7 +160,7 @@ spoe-agent edgeguard-waf-agent
|
||||
use-backend spoe-edgeguard-waf
|
||||
|
||||
spoe-message edgeguard-waf-req
|
||||
args src=src method=method uri=url ver=req.ver headers=req.hdrs host=req.hdr(host)
|
||||
args src=src method=method uri=url ver=req.ver headers=req.hdrs host=req.hdr(host) body=req.body
|
||||
event on-frontend-http-request
|
||||
`
|
||||
|
||||
|
||||
@@ -554,6 +554,75 @@ func TestRender_WebSocketEmitsTunnelTimeout(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_ServerTimeoutOverride(t *testing.T) {
|
||||
tmo := 300
|
||||
v := View{
|
||||
Backends: []BackendView{
|
||||
{
|
||||
Backend: models.Backend{ID: 11, Name: "ai", Scheme: "http",
|
||||
LBAlgorithm: "roundrobin", ServerTimeoutSeconds: &tmo, Active: true},
|
||||
Servers: []models.BackendServer{
|
||||
{BackendID: 11, Name: "ai-1", Address: "10.0.5.30", Port: 8000, Weight: 100, Active: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Backend: models.Backend{ID: 12, Name: "web", Scheme: "http",
|
||||
LBAlgorithm: "roundrobin", Active: true},
|
||||
Servers: []models.BackendServer{
|
||||
{BackendID: 12, Name: "web-1", Address: "10.0.5.31", Port: 8080, Weight: 100, Active: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
out := renderView(t, v)
|
||||
idxAI := strings.Index(out, "backend eg_backend_11")
|
||||
idxWeb := strings.Index(out, "backend eg_backend_12")
|
||||
if idxAI < 0 || idxWeb < 0 {
|
||||
t.Fatalf("backend sections missing in output:\n%s", out)
|
||||
}
|
||||
aiBlock := out[idxAI:idxWeb]
|
||||
webBlock := out[idxWeb:]
|
||||
// ai (nil-Override gesetzt) soll `timeout server 300s` bekommen …
|
||||
if !strings.Contains(aiBlock, "timeout server 300s") {
|
||||
t.Errorf("ai-Block sollte `timeout server 300s` enthalten:\n%s", aiBlock)
|
||||
}
|
||||
// … web (kein Override) soll KEINE eigene timeout-server-Zeile bekommen.
|
||||
if strings.Contains(webBlock, "timeout server") {
|
||||
t.Errorf("web-Block soll KEIN eigenes `timeout server` enthalten:\n%s", webBlock)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_WAFBuffersRequestBody(t *testing.T) {
|
||||
// Ohne WAF: kein Body-Buffering (kein RAM-Overhead).
|
||||
off := renderView(t, View{WAFEnabled: false})
|
||||
if strings.Contains(off, "option http-buffer-request") {
|
||||
t.Errorf("ohne WAF darf kein `option http-buffer-request` gerendert werden:\n%s", off)
|
||||
}
|
||||
if strings.Contains(off, "filter spoe") {
|
||||
t.Errorf("ohne WAF darf kein SPOE-Filter gerendert werden")
|
||||
}
|
||||
|
||||
// Mit WAF: Body-Buffering VOR dem SPOE-Filter, damit req.body verfügbar ist.
|
||||
on := renderView(t, View{WAFEnabled: true})
|
||||
idxBuf := strings.Index(on, "option http-buffer-request")
|
||||
idxFilter := strings.Index(on, "filter spoe engine edgeguard-waf")
|
||||
if idxBuf < 0 {
|
||||
t.Fatalf("mit WAF muss `option http-buffer-request` gerendert werden:\n%s", on)
|
||||
}
|
||||
if idxFilter < 0 {
|
||||
t.Fatalf("mit WAF muss der SPOE-Filter gerendert werden")
|
||||
}
|
||||
if idxBuf > idxFilter {
|
||||
t.Errorf("`option http-buffer-request` muss VOR dem SPOE-Filter stehen (buf=%d filter=%d)", idxBuf, idxFilter)
|
||||
}
|
||||
|
||||
// Die SPOE-Message muss den Body an den Agent schicken, sonst kann
|
||||
// Coraza ihn nicht inspizieren.
|
||||
if !strings.Contains(spoeCfg, "body=req.body") {
|
||||
t.Errorf("spoeCfg muss `body=req.body` an den WAF-Agent senden:\n%s", spoeCfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_MultiServerPool(t *testing.T) {
|
||||
v := View{
|
||||
Backends: []BackendView{
|
||||
|
||||
@@ -45,7 +45,7 @@ vrrp_instance VI_1 {
|
||||
virtual_router_id {{ .RouterID }}
|
||||
priority {{ .Priority }}
|
||||
advert_int 2
|
||||
nopreempt
|
||||
{{ if .PreemptDelay }}preempt_delay {{ .PreemptDelay }}{{ else }}nopreempt{{ end }}
|
||||
{{ if .SrcIP }} unicast_src_ip {{ .SrcIP }}
|
||||
unicast_peer {
|
||||
{{ .PeerIP }}
|
||||
@@ -72,7 +72,7 @@ vrrp_instance VI_HB {
|
||||
virtual_router_id {{ .HBRouterID }}
|
||||
priority {{ .Priority }}
|
||||
advert_int 2
|
||||
nopreempt
|
||||
{{ if .PreemptDelay }}preempt_delay {{ .PreemptDelay }}{{ else }}nopreempt{{ end }}
|
||||
{{ if .HBSrcIP }} unicast_src_ip {{ .HBSrcIP }}
|
||||
unicast_peer {
|
||||
{{ .HBPeerIP }}
|
||||
|
||||
@@ -54,8 +54,19 @@ type View struct {
|
||||
HBRouterID int
|
||||
// GW-Tracking
|
||||
GWCheckIP string
|
||||
// PreemptDelay > 0 → Node holt die VIP nach Erholung zurück (nach N Sekunden
|
||||
// Stabilität). 0 → nopreempt (bleibt Backup). Siehe buildView.
|
||||
PreemptDelay int
|
||||
}
|
||||
|
||||
// preemptDelaySeconds: der bevorzugte Node (höhere Priorität = PG-Primary) holt
|
||||
// die VIP erst nach dieser Wartezeit zurück — lange genug, dass ein frisch
|
||||
// gebooteter/deployter Node erst wirklich bereit ist (Boot + Service-Start),
|
||||
// bevor er überhaupt preempten darf. Zusammen mit dem gehärteten Health-Check
|
||||
// (haproxy aktiv + :443 gebunden, keepalived-check.sh) verhindert das den
|
||||
// Incident 2026-08-03 (halb-kaputter Node riss die VIP an sich).
|
||||
const preemptDelaySeconds = 120
|
||||
|
||||
type generator struct {
|
||||
pool *pgxpool.Pool
|
||||
localID string
|
||||
@@ -167,14 +178,21 @@ func (g *generator) buildView(cs *models.ClusterSettings, vips []VIPEntry, local
|
||||
v.HBRouterID = 52
|
||||
}
|
||||
|
||||
// State IMMER BACKUP: das Template setzt `nopreempt`, und nopreempt wirkt
|
||||
// in keepalived NUR, wenn die Instanz im BACKUP-Zustand startet (bei state
|
||||
// MASTER wird nopreempt ignoriert). Die Priorität entscheidet weiterhin die
|
||||
// Initial-Election (primary=200 gewinnt), aber ein erholter Node reißt die
|
||||
// VIP NICHT mehr zurück → kein Flap-Back / Split-Brain. Deckt sich mit der
|
||||
// "kein Auto-Promote"-Philosophie: Promotion bleibt manuell.
|
||||
// pg_role=standby ist das härtere Signal (Standby ist nie bevorzugter Node).
|
||||
// State IMMER BACKUP; die Priorität entscheidet, welcher Node die VIP
|
||||
// bevorzugt hält (PG-Primary=200 > Standby=100). Mit preempt_delay holt
|
||||
// der bevorzugte Node die VIP nach Erholung zurück (VIP-Affinität zum
|
||||
// PG-Primary), aber erst nach preemptDelaySeconds Stabilität.
|
||||
//
|
||||
// Incident 2026-08-03 & Fix: eine frühere preempt_delay-Variante ließ den
|
||||
// Prio-200-Node die VIP zurückholen, sobald der Health-Check ihn für
|
||||
// „gesund" hielt — der prüfte aber NUR die edgeguard-api, nicht ob der Node
|
||||
// wirklich Traffic bedient. Ein halb-kaputter Primary (api up, haproxy/Netz
|
||||
// down) riss so die VIP an sich → Ausfall. Preempt ist wieder aktiv, WEIL
|
||||
// keepalived-check.sh jetzt zusätzlich haproxy-aktiv + :443-gebunden fordert:
|
||||
// ein nicht-bedienender Node geht in FAULT und kann NICHT preempten.
|
||||
// Promotion/PG-Failover bleibt manuell (edgeguard-ctl promote).
|
||||
v.State = "BACKUP"
|
||||
v.PreemptDelay = preemptDelaySeconds
|
||||
if local.PGRole == "standby" {
|
||||
v.Priority = 100
|
||||
} else if local.PGRole == "primary" || local.Role == "primary" {
|
||||
|
||||
@@ -17,32 +17,52 @@ func render(t *testing.T, v View) string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Split-Brain-Schutz: jede vrrp_instance MUSS `nopreempt` tragen, sonst reißt
|
||||
// ein erholter Node die VIP zurück → Flapping. nopreempt wirkt nur bei state
|
||||
// BACKUP — also muss auch der bevorzugte Node BACKUP starten.
|
||||
func testView() View {
|
||||
return View{
|
||||
State: "BACKUP", Interface: "eth0", RouterID: 51, Priority: 200,
|
||||
SrcIP: "89.163.205.6", PeerIP: "89.163.205.8", AuthPass: "edgeguard",
|
||||
VIPs: []VIPEntry{{Address: "89.163.205.100", Prefix: 24, Device: "eth0"}},
|
||||
HBInterface: "ens19", HBSrcIP: "169.254.0.1", HBPeerIP: "169.254.0.2", HBRouterID: 52,
|
||||
GWCheckIP: "89.163.205.1",
|
||||
GWCheckIP: "89.163.205.1", PreemptDelay: 120,
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateNopreemptOnBothInstances(t *testing.T) {
|
||||
// VIP-Affinität zum PG-Primary (Incident-2026-08-03-Fix): mit PreemptDelay
|
||||
// tragen BEIDE Instanzen `preempt_delay N` statt nopreempt, damit der
|
||||
// bevorzugte Node die VIP nach Erholung zurückholt — aber erst nach N Sekunden
|
||||
// Stabilität. Kein Node darf `state MASTER` starten (sonst kein sauberes
|
||||
// Election). Preempt ist nur sicher, WEIL keepalived-check.sh haproxy-Bereit-
|
||||
// schaft (aktiv + :443) mitprüft (siehe dortiger Kommentar).
|
||||
func TestTemplatePreemptDelayOnBothInstances(t *testing.T) {
|
||||
out := render(t, testView())
|
||||
if n := strings.Count(out, "nopreempt"); n != 2 {
|
||||
t.Fatalf("erwarte nopreempt in VI_1 UND VI_HB (2×), gefunden: %d\n%s", n, out)
|
||||
if c := strings.Count(out, "preempt_delay 120"); c != 2 {
|
||||
t.Fatalf("erwarte preempt_delay 120 in VI_1 UND VI_HB (2×), gefunden: %d\n%s", c, out)
|
||||
}
|
||||
if strings.Contains(out, "nopreempt") {
|
||||
t.Fatalf("bei PreemptDelay>0 darf KEIN nopreempt gerendert werden:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "state MASTER") {
|
||||
t.Fatalf("kein Node darf state MASTER starten (nopreempt würde ignoriert):\n%s", out)
|
||||
t.Fatalf("kein Node darf state MASTER starten:\n%s", out)
|
||||
}
|
||||
if c := strings.Count(out, "state BACKUP"); c != 2 {
|
||||
t.Fatalf("erwarte state BACKUP in beiden Instanzen, gefunden: %d", c)
|
||||
}
|
||||
}
|
||||
|
||||
// Ohne PreemptDelay (==0) fällt das Template auf nopreempt zurück (Node bleibt
|
||||
// Backup, keine VIP-Rückkehr) — der sichere Default, falls Preempt je aus soll.
|
||||
func TestTemplateFallsBackToNopreempt(t *testing.T) {
|
||||
v := testView()
|
||||
v.PreemptDelay = 0
|
||||
out := render(t, v)
|
||||
if c := strings.Count(out, "nopreempt"); c != 2 {
|
||||
t.Fatalf("erwarte nopreempt in beiden Instanzen (2×) bei PreemptDelay=0, gefunden: %d\n%s", c, out)
|
||||
}
|
||||
if strings.Contains(out, "preempt_delay") {
|
||||
t.Fatalf("bei PreemptDelay=0 darf KEIN preempt_delay gerendert werden:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// GARP muss forciert + periodic aufgefrischt werden, sonst altert die
|
||||
// VIP-MAC am Upstream-Switch und die Failover-IP wird unerreichbar.
|
||||
func TestTemplateGARPRefresh(t *testing.T) {
|
||||
@@ -103,10 +123,13 @@ func TestBuildViewStateAlwaysBackup(t *testing.T) {
|
||||
local := &models.HANode{ID: "n1", PGRole: c.pgRole, Role: c.role, PublicIP: &pub}
|
||||
v := g.buildView(cs, nil, local, nil)
|
||||
if v.State != "BACKUP" {
|
||||
t.Errorf("pg_role=%q role=%q: State=%q, erwarte immer BACKUP (nopreempt)", c.pgRole, c.role, v.State)
|
||||
t.Errorf("pg_role=%q role=%q: State=%q, erwarte immer BACKUP", c.pgRole, c.role, v.State)
|
||||
}
|
||||
if v.Priority != c.wantPrio {
|
||||
t.Errorf("pg_role=%q role=%q: Priority=%d, erwarte %d", c.pgRole, c.role, v.Priority, c.wantPrio)
|
||||
}
|
||||
if v.PreemptDelay != 120 {
|
||||
t.Errorf("pg_role=%q role=%q: PreemptDelay=%d, erwarte 120", c.pgRole, c.role, v.PreemptDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ type Backend struct {
|
||||
LBAlgorithm string `gorm:"column:lb_algorithm" json:"lb_algorithm"`
|
||||
WebSocket bool `gorm:"column:websocket" json:"websocket"`
|
||||
ForceHTTP1 bool `gorm:"column:force_http1" json:"force_http1"`
|
||||
// ServerTimeoutSeconds überschreibt `timeout server` für dieses
|
||||
// Backend (Sekunden). nil = defaults-Timeout (60s). Für langsam
|
||||
// antwortende Upstreams (KI-/Inferenz-Server ohne Streaming).
|
||||
ServerTimeoutSeconds *int `gorm:"column:server_timeout_seconds" json:"server_timeout_seconds,omitempty"`
|
||||
Active bool `gorm:"column:active" json:"active"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
|
||||
@@ -20,6 +20,9 @@ type Domain struct {
|
||||
DisableH3 bool `gorm:"column:disable_h3" json:"disable_h3"`
|
||||
Notes *string `gorm:"column:notes" json:"notes,omitempty"`
|
||||
RedirectTo string `gorm:"column:redirect_to" json:"redirect_to"` // ""=aus; sonst 301-Ziel-URL (Domain→Domain)
|
||||
// CrowdSecTrusted: vertrauenswürdiges Admin-Panel → dessen Hostname wird in
|
||||
// die CrowdSec-Whitelist gerendert (Admin-SPA-Traffic ist kein Crawl).
|
||||
CrowdSecTrusted bool `gorm:"column:crowdsec_trusted" json:"crowdsec_trusted"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -11,6 +11,14 @@ type WafConfig struct {
|
||||
Mode string `gorm:"column:mode" json:"mode"` // "detection" | "blocking"
|
||||
ParanoiaLevel int `gorm:"column:paranoia_level" json:"paranoia_level"` // 1–4
|
||||
RuleExclusions []string `gorm:"column:rule_exclusions;type:text[]" json:"rule_exclusions"`
|
||||
// CRSPlugins: aktivierte OWASP-CRS-App-Exclusion-Plugins (z. B.
|
||||
// "nextcloud","wordpress"). Der Renderer inkludiert je Plugin dessen
|
||||
// config/before/after-Dateien aus <crsDir>/plugins/.
|
||||
CRSPlugins []string `gorm:"column:crs_plugins;type:text[]" json:"crs_plugins"`
|
||||
// AppProfiles: zugewiesene benutzerdefinierte WAF-App-Profile (Namen aus
|
||||
// waf_app_profiles). Ihre rule_exclusions werden im Agent in die effektiven
|
||||
// Ausnahmen dieser Domain gemischt.
|
||||
AppProfiles []string `gorm:"column:app_profiles;type:text[]" json:"app_profiles"`
|
||||
ExclusionNotes map[string]string `gorm:"column:exclusion_notes;type:jsonb" json:"exclusion_notes"` // rule_id → note
|
||||
TrustedProxies []string `gorm:"column:trusted_proxies;type:text[]" json:"trusted_proxies"`
|
||||
CustomRules string `gorm:"column:custom_rules" json:"custom_rules"`
|
||||
@@ -18,3 +26,18 @@ type WafConfig struct {
|
||||
}
|
||||
|
||||
func (WafConfig) TableName() string { return "waf_configs" }
|
||||
|
||||
// WafAppProfile ist ein benanntes, wiederverwendbares Bündel von CRS-Rule-
|
||||
// Exclusions (reine Rule-IDs/Ranges). Built-in-Profile (builtin=true) sind
|
||||
// read-only; benutzerdefinierte sind im UI editierbar und pro Domain zuweisbar.
|
||||
type WafAppProfile struct {
|
||||
ID int64 `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"column:name;uniqueIndex" json:"name"`
|
||||
Description string `gorm:"column:description" json:"description"`
|
||||
RuleExclusions []string `gorm:"column:rule_exclusions;type:text[]" json:"rule_exclusions"`
|
||||
Builtin bool `gorm:"column:builtin" json:"builtin"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WafAppProfile) TableName() string { return "waf_app_profiles" }
|
||||
|
||||
@@ -62,13 +62,14 @@ type EmailSettings struct {
|
||||
|
||||
// Event ist eine Row in alert_events.
|
||||
type Event struct {
|
||||
ID int64 `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Severity Severity `json:"severity"`
|
||||
Subject string `json:"subject"`
|
||||
Message string `json:"message"`
|
||||
SentTo json.RawMessage `json:"sent_to"`
|
||||
FiredAt time.Time `json:"fired_at"`
|
||||
ID int64 `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Severity Severity `json:"severity"`
|
||||
Subject string `json:"subject"`
|
||||
Message string `json:"message"`
|
||||
SentTo json.RawMessage `json:"sent_to"`
|
||||
FiredAt time.Time `json:"fired_at"`
|
||||
AcknowledgedAt *time.Time `json:"acknowledged_at,omitempty"`
|
||||
}
|
||||
|
||||
// SendResult pro Channel — landet als JSON-Array in sent_to.
|
||||
@@ -170,14 +171,19 @@ func (s *Service) DeleteChannel(ctx context.Context, id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListEvents liefert die letzten N Events newest-first.
|
||||
func (s *Service) ListEvents(ctx context.Context, limit int) ([]Event, error) {
|
||||
// ListEvents liefert die letzten N Events newest-first. Wenn openOnly
|
||||
// gesetzt ist, werden nur noch offene (nicht quittierte) Events geliefert —
|
||||
// das nutzt die Dashboard-Karte, damit Quittieren die Meldung verschwinden
|
||||
// lässt.
|
||||
func (s *Service) ListEvents(ctx context.Context, limit int, openOnly bool) ([]Event, error) {
|
||||
if limit <= 0 || limit > 500 {
|
||||
limit = 100
|
||||
}
|
||||
rows, err := s.Pool.Query(ctx, `
|
||||
SELECT id, kind, severity, subject, message, sent_to, fired_at
|
||||
FROM alert_events ORDER BY fired_at DESC, id DESC LIMIT $1`, limit)
|
||||
SELECT id, kind, severity, subject, message, sent_to, fired_at, acknowledged_at
|
||||
FROM alert_events
|
||||
WHERE ($2::bool = false OR acknowledged_at IS NULL)
|
||||
ORDER BY fired_at DESC, id DESC LIMIT $1`, limit, openOnly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -186,7 +192,7 @@ FROM alert_events ORDER BY fired_at DESC, id DESC LIMIT $1`, limit)
|
||||
for rows.Next() {
|
||||
var e Event
|
||||
if err := rows.Scan(&e.ID, &e.Kind, &e.Severity, &e.Subject,
|
||||
&e.Message, &e.SentTo, &e.FiredAt); err != nil {
|
||||
&e.Message, &e.SentTo, &e.FiredAt, &e.AcknowledgedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, e)
|
||||
@@ -194,6 +200,64 @@ FROM alert_events ORDER BY fired_at DESC, id DESC LIMIT $1`, limit)
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Acknowledge quittiert die angegebenen Events (setzt acknowledged_at=NOW()
|
||||
// bei noch offenen). Liefert die Anzahl geänderter Rows.
|
||||
func (s *Service) Acknowledge(ctx context.Context, ids []int64) (int64, error) {
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
tag, err := s.Pool.Exec(ctx,
|
||||
`UPDATE alert_events SET acknowledged_at = NOW()
|
||||
WHERE id = ANY($1) AND acknowledged_at IS NULL`, ids)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return tag.RowsAffected(), nil
|
||||
}
|
||||
|
||||
// AcknowledgeAll quittiert alle offenen Events — Backing für den
|
||||
// "Alle quittieren"-Button.
|
||||
func (s *Service) AcknowledgeAll(ctx context.Context) (int64, error) {
|
||||
tag, err := s.Pool.Exec(ctx,
|
||||
`UPDATE alert_events SET acknowledged_at = NOW() WHERE acknowledged_at IS NULL`)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return tag.RowsAffected(), nil
|
||||
}
|
||||
|
||||
// DeleteEvents löscht die angegebenen Events endgültig. Liefert die Anzahl
|
||||
// gelöschter Rows.
|
||||
func (s *Service) DeleteEvents(ctx context.Context, ids []int64) (int64, error) {
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
tag, err := s.Pool.Exec(ctx,
|
||||
`DELETE FROM alert_events WHERE id = ANY($1)`, ids)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return tag.RowsAffected(), nil
|
||||
}
|
||||
|
||||
// Cleanup löscht alert_events älter als keepDays und liefert die Anzahl
|
||||
// gelöschter Rows. make_interval(days => $1) nimmt $1 sauber als int —
|
||||
// der frühere ($1 || ' days')::interval-Ansatz erzwang text und scheiterte
|
||||
// unter pgx mit einem Encode-Fehler (vgl. waf PurgeAlerts, v1.3.3).
|
||||
func (s *Service) Cleanup(ctx context.Context, keepDays int) (int64, error) {
|
||||
if keepDays <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
tag, err := s.Pool.Exec(ctx,
|
||||
`DELETE FROM alert_events WHERE fired_at < NOW() - make_interval(days => $1)`,
|
||||
keepDays,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return tag.RowsAffected(), nil
|
||||
}
|
||||
|
||||
// Fire dispatch'ed einen Event an alle aktiven Channels und persistiert
|
||||
// das Ergebnis. Non-fatal — Send-Failures werden im sent_to-JSON
|
||||
// dokumentiert, der Event selbst landet in jedem Fall in der History.
|
||||
|
||||
@@ -237,3 +237,61 @@ func AutoUpdateEnabled() bool {
|
||||
_, err := os.Stat(AutoUpdateConfPath)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ── Update-Kanal (stable/testing) ──────────────────────────────────────
|
||||
//
|
||||
// Kanal-Modell 1:1 von enconf übernommen: Suite = OS-Codename (trixie),
|
||||
// Komponente = Kanal. Kein eigenes Config-File — die sources.list-Zeile
|
||||
// selbst ist die einzige Quelle der Wahrheit (siehe scripts/install.sh
|
||||
// setup_repo(), das dieselbe Zeile beim Erstinstall schreibt).
|
||||
|
||||
// SourcesListPath: vom Installer angelegte apt-Quelle. Root-owned wie
|
||||
// AutoUpdateConfPath — Schreibzugriff nur via sudo tee (Sudoers-Pin im
|
||||
// postinst).
|
||||
const SourcesListPath = "/etc/apt/sources.list.d/edgeguard.list"
|
||||
|
||||
const sourcesListTemplate = "deb [signed-by=/etc/apt/keyrings/nmg.asc] " +
|
||||
"https://git.netcell-it.de/api/packages/projekte/debian trixie %s\n"
|
||||
|
||||
// Channel liest den aktuell konfigurierten Update-Kanal aus dem letzten
|
||||
// Feld der deb-Zeile. Default "stable" wenn die Datei fehlt oder das
|
||||
// letzte Feld kein bekannter Kanal ist (Fail-safe — nie stillschweigend
|
||||
// "testing" annehmen).
|
||||
func Channel() string {
|
||||
data, err := os.ReadFile(SourcesListPath)
|
||||
if err != nil {
|
||||
return "stable"
|
||||
}
|
||||
for _, raw := range strings.Split(string(data), "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
if !strings.HasPrefix(line, "deb ") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 {
|
||||
continue
|
||||
}
|
||||
switch fields[len(fields)-1] {
|
||||
case "stable", "testing":
|
||||
return fields[len(fields)-1]
|
||||
}
|
||||
}
|
||||
return "stable"
|
||||
}
|
||||
|
||||
// SetChannel schreibt die sources.list-Zeile mit dem neuen Kanal und
|
||||
// refresht den apt-Cache sofort — sonst zeigt der Update-Banner bis zum
|
||||
// nächsten 5-min-Throttle-Fenster noch den alten Kanal-Stand.
|
||||
func SetChannel(ctx context.Context, channel string) error {
|
||||
if channel != "stable" && channel != "testing" {
|
||||
return fmt.Errorf("apt: unknown channel %q (expected stable|testing)", channel)
|
||||
}
|
||||
body := fmt.Sprintf(sourcesListTemplate, channel)
|
||||
cmd := exec.Command("sudo", "-n", "/usr/bin/tee", SourcesListPath)
|
||||
cmd.Stdin = strings.NewReader(body)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("sudo tee %s: %w: %s", SourcesListPath, err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
RefreshNow(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ type Repo struct {
|
||||
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
|
||||
|
||||
const baseSelect = `
|
||||
SELECT id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active,
|
||||
created_at, updated_at
|
||||
SELECT id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1,
|
||||
server_timeout_seconds, active, created_at, updated_at
|
||||
FROM backends
|
||||
`
|
||||
|
||||
@@ -65,11 +65,13 @@ func (r *Repo) Create(ctx context.Context, b models.Backend) (*models.Backend, e
|
||||
b.LBAlgorithm = "roundrobin"
|
||||
}
|
||||
row := r.Pool.QueryRow(ctx, `
|
||||
INSERT INTO backends (name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active,
|
||||
created_at, updated_at`,
|
||||
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.ForceHTTP1, b.Active)
|
||||
INSERT INTO backends (name, scheme, health_check_path, lb_algorithm, websocket, force_http1,
|
||||
server_timeout_seconds, active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1,
|
||||
server_timeout_seconds, active, created_at, updated_at`,
|
||||
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.ForceHTTP1,
|
||||
b.ServerTimeoutSeconds, b.Active)
|
||||
return scanBackend(row)
|
||||
}
|
||||
|
||||
@@ -85,12 +87,14 @@ UPDATE backends SET
|
||||
lb_algorithm = $4,
|
||||
websocket = $5,
|
||||
force_http1 = $6,
|
||||
active = $7,
|
||||
server_timeout_seconds = $7,
|
||||
active = $8,
|
||||
updated_at = NOW()
|
||||
WHERE id = $8
|
||||
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active,
|
||||
created_at, updated_at`,
|
||||
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.ForceHTTP1, b.Active, id)
|
||||
WHERE id = $9
|
||||
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1,
|
||||
server_timeout_seconds, active, created_at, updated_at`,
|
||||
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.ForceHTTP1,
|
||||
b.ServerTimeoutSeconds, b.Active, id)
|
||||
out, err := scanBackend(row)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
@@ -125,7 +129,8 @@ func scanBackend(row interface{ Scan(...any) error }) (*models.Backend, error) {
|
||||
var b models.Backend
|
||||
if err := row.Scan(
|
||||
&b.ID, &b.Name, &b.Scheme,
|
||||
&b.HealthCheckPath, &b.LBAlgorithm, &b.WebSocket, &b.ForceHTTP1, &b.Active,
|
||||
&b.HealthCheckPath, &b.LBAlgorithm, &b.WebSocket, &b.ForceHTTP1,
|
||||
&b.ServerTimeoutSeconds, &b.Active,
|
||||
&b.CreatedAt, &b.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -24,7 +24,7 @@ SELECT id, name, active, primary_backend_id, http_to_https,
|
||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||
maintenance_mode, maintenance_message, www_redirect,
|
||||
rate_limit_rps, max_body_kb, disable_h3,
|
||||
notes, redirect_to, created_at, updated_at
|
||||
notes, redirect_to, crowdsec_trusted, created_at, updated_at
|
||||
FROM domains
|
||||
`
|
||||
|
||||
@@ -65,17 +65,19 @@ func (r *Repo) Create(ctx context.Context, d models.Domain) (*models.Domain, err
|
||||
INSERT INTO domains (name, active, primary_backend_id, http_to_https,
|
||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||
maintenance_mode, maintenance_message, www_redirect,
|
||||
rate_limit_rps, max_body_kb, disable_h3, notes, redirect_to)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
rate_limit_rps, max_body_kb, disable_h3, notes, redirect_to,
|
||||
crowdsec_trusted)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||
RETURNING id, name, active, primary_backend_id, http_to_https,
|
||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||
maintenance_mode, maintenance_message, www_redirect,
|
||||
rate_limit_rps, max_body_kb, disable_h3,
|
||||
notes, redirect_to, created_at, updated_at`,
|
||||
notes, redirect_to, crowdsec_trusted, created_at, updated_at`,
|
||||
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
|
||||
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
|
||||
d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect,
|
||||
d.RateLimitRPS, d.MaxBodyKB, d.DisableH3, d.Notes, d.RedirectTo)
|
||||
d.RateLimitRPS, d.MaxBodyKB, d.DisableH3, d.Notes, d.RedirectTo,
|
||||
d.CrowdSecTrusted)
|
||||
return scanDomain(row)
|
||||
}
|
||||
|
||||
@@ -101,17 +103,19 @@ UPDATE domains SET
|
||||
disable_h3 = $14,
|
||||
notes = $15,
|
||||
redirect_to = $16,
|
||||
crowdsec_trusted = $17,
|
||||
updated_at = NOW()
|
||||
WHERE id = $17
|
||||
WHERE id = $18
|
||||
RETURNING id, name, active, primary_backend_id, http_to_https,
|
||||
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
|
||||
maintenance_mode, maintenance_message, www_redirect,
|
||||
rate_limit_rps, max_body_kb, disable_h3,
|
||||
notes, redirect_to, created_at, updated_at`,
|
||||
notes, redirect_to, crowdsec_trusted, created_at, updated_at`,
|
||||
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
|
||||
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
|
||||
d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect,
|
||||
d.RateLimitRPS, d.MaxBodyKB, d.DisableH3, d.Notes, d.RedirectTo, id)
|
||||
d.RateLimitRPS, d.MaxBodyKB, d.DisableH3, d.Notes, d.RedirectTo,
|
||||
d.CrowdSecTrusted, id)
|
||||
out, err := scanDomain(row)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
@@ -140,7 +144,7 @@ func scanDomain(row interface{ Scan(...any) error }) (*models.Domain, error) {
|
||||
&d.HSTSEnabled, &d.HSTSMaxAge, &d.HSTSSubdomains, &d.HSTSPreload,
|
||||
&d.MaintenanceMode, &d.MaintenanceMessage, &d.WWWRedirect,
|
||||
&d.RateLimitRPS, &d.MaxBodyKB, &d.DisableH3,
|
||||
&d.Notes, &d.RedirectTo, &d.CreatedAt, &d.UpdatedAt,
|
||||
&d.Notes, &d.RedirectTo, &d.CrowdSecTrusted, &d.CreatedAt, &d.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
124
internal/services/waf/appprofiles.go
Normal file
124
internal/services/waf/appprofiles.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||
)
|
||||
|
||||
// ErrProfileNotFound wird von Get/Update/Delete zurückgegeben, wenn kein Profil
|
||||
// mit der ID existiert.
|
||||
var ErrProfileNotFound = errors.New("waf app profile not found")
|
||||
|
||||
const profileSelect = `
|
||||
SELECT id, name, description, rule_exclusions, builtin, created_at, updated_at
|
||||
FROM waf_app_profiles
|
||||
`
|
||||
|
||||
func scanProfile(row pgx.Row) (*models.WafAppProfile, error) {
|
||||
var p models.WafAppProfile
|
||||
if err := row.Scan(&p.ID, &p.Name, &p.Description, &p.RuleExclusions, &p.Builtin, &p.CreatedAt, &p.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// ListProfiles gibt alle App-Profile zurück (Built-in zuerst, dann alphabetisch).
|
||||
func (r *Repo) ListProfiles(ctx context.Context) ([]models.WafAppProfile, error) {
|
||||
rows, err := r.Pool.Query(ctx, profileSelect+" ORDER BY builtin DESC, name ASC")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]models.WafAppProfile, 0, 16)
|
||||
for rows.Next() {
|
||||
p, err := scanProfile(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, *p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// profilesByName lädt alle Profile in eine Name→Profil-Map (für die Auflösung
|
||||
// im Agent-Loader).
|
||||
func (r *Repo) profilesByName(ctx context.Context) (map[string]models.WafAppProfile, error) {
|
||||
list, err := r.ListProfiles(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]models.WafAppProfile, len(list))
|
||||
for _, p := range list {
|
||||
m[p.Name] = p
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// GetProfile gibt ein Profil per ID zurück, oder ErrProfileNotFound.
|
||||
func (r *Repo) GetProfile(ctx context.Context, id int64) (*models.WafAppProfile, error) {
|
||||
row := r.Pool.QueryRow(ctx, profileSelect+" WHERE id = $1", id)
|
||||
p, err := scanProfile(row)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, ErrProfileNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// CreateProfile legt ein neues benutzerdefiniertes Profil an (builtin immer
|
||||
// false — Built-ins werden nicht über die API erzeugt).
|
||||
func (r *Repo) CreateProfile(ctx context.Context, name, description string, exclusions []string) (*models.WafAppProfile, error) {
|
||||
if exclusions == nil {
|
||||
exclusions = []string{}
|
||||
}
|
||||
now := time.Now()
|
||||
row := r.Pool.QueryRow(ctx, `
|
||||
INSERT INTO waf_app_profiles (name, description, rule_exclusions, builtin, created_at, updated_at)
|
||||
VALUES ($1,$2,$3,false,$4,$4)
|
||||
RETURNING id, name, description, rule_exclusions, builtin, created_at, updated_at
|
||||
`, name, description, exclusions, now)
|
||||
return scanProfile(row)
|
||||
}
|
||||
|
||||
// UpdateProfile ändert Name/Beschreibung/Ausnahmen eines Profils. Built-in-
|
||||
// Profile sind read-only (WHERE builtin = false) → ErrProfileNotFound, wenn
|
||||
// das Profil fehlt ODER built-in ist.
|
||||
func (r *Repo) UpdateProfile(ctx context.Context, id int64, name, description string, exclusions []string) (*models.WafAppProfile, error) {
|
||||
if exclusions == nil {
|
||||
exclusions = []string{}
|
||||
}
|
||||
row := r.Pool.QueryRow(ctx, `
|
||||
UPDATE waf_app_profiles
|
||||
SET name = $2, description = $3, rule_exclusions = $4, updated_at = $5
|
||||
WHERE id = $1 AND builtin = false
|
||||
RETURNING id, name, description, rule_exclusions, builtin, created_at, updated_at
|
||||
`, id, name, description, exclusions, time.Now())
|
||||
p, err := scanProfile(row)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, ErrProfileNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// DeleteProfile entfernt ein benutzerdefiniertes Profil. Built-in-Profile sind
|
||||
// geschützt. Gibt ErrProfileNotFound zurück, wenn nichts gelöscht wurde.
|
||||
func (r *Repo) DeleteProfile(ctx context.Context, id int64) error {
|
||||
tag, err := r.Pool.Exec(ctx, `DELETE FROM waf_app_profiles WHERE id = $1 AND builtin = false`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return ErrProfileNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
48
internal/services/waf/appprofiles_test.go
Normal file
48
internal/services/waf/appprofiles_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||
)
|
||||
|
||||
func TestMergeProfileExclusions(t *testing.T) {
|
||||
base := time.Date(2026, 8, 3, 10, 0, 0, 0, time.UTC)
|
||||
newer := base.Add(1 * time.Hour)
|
||||
profiles := map[string]models.WafAppProfile{
|
||||
"nc": {Name: "nc", RuleExclusions: []string{"942100", "920420"}, UpdatedAt: newer},
|
||||
"wp": {Name: "wp", RuleExclusions: []string{"942100", "941100"}, UpdatedAt: base},
|
||||
}
|
||||
|
||||
t.Run("keine Profile → unverändert", func(t *testing.T) {
|
||||
got, ts := mergeProfileExclusions([]string{"1000"}, base, nil, profiles)
|
||||
if len(got) != 1 || got[0] != "1000" || !ts.Equal(base) {
|
||||
t.Fatalf("got=%v ts=%v", got, ts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("union dedupliziert, Reihenfolge stabil", func(t *testing.T) {
|
||||
got, ts := mergeProfileExclusions([]string{"942100", "900001"}, base, []string{"nc", "wp"}, profiles)
|
||||
want := []string{"942100", "900001", "920420", "941100"} // 942100 nicht doppelt
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("got=%v want=%v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("got=%v want=%v", got, want)
|
||||
}
|
||||
}
|
||||
// effektives updated_at = max(base, nc.newer) = newer
|
||||
if !ts.Equal(newer) {
|
||||
t.Fatalf("ts=%v want=%v (Profil-Edit muss Rebuild ausloesen)", ts, newer)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unbekanntes Profil defensiv ignoriert", func(t *testing.T) {
|
||||
got, ts := mergeProfileExclusions([]string{"1000"}, base, []string{"gibtsnicht"}, profiles)
|
||||
if len(got) != 1 || got[0] != "1000" || !ts.Equal(base) {
|
||||
t.Fatalf("got=%v ts=%v", got, ts)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
|
||||
|
||||
const baseSelect = `
|
||||
SELECT id, domain_id, enabled, mode, paranoia_level,
|
||||
rule_exclusions, exclusion_notes, trusted_proxies, custom_rules, updated_at
|
||||
rule_exclusions, crs_plugins, app_profiles, exclusion_notes, trusted_proxies, custom_rules, updated_at
|
||||
FROM waf_configs
|
||||
`
|
||||
|
||||
@@ -30,7 +30,7 @@ func scan(row pgx.Row) (*models.WafConfig, error) {
|
||||
var c models.WafConfig
|
||||
err := row.Scan(
|
||||
&c.ID, &c.DomainID, &c.Enabled, &c.Mode, &c.ParanoiaLevel,
|
||||
&c.RuleExclusions, &c.ExclusionNotes, &c.TrustedProxies, &c.CustomRules, &c.UpdatedAt,
|
||||
&c.RuleExclusions, &c.CRSPlugins, &c.AppProfiles, &c.ExclusionNotes, &c.TrustedProxies, &c.CustomRules, &c.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -82,22 +82,24 @@ func (r *Repo) Upsert(ctx context.Context, c models.WafConfig) (*models.WafConfi
|
||||
row := r.Pool.QueryRow(ctx, `
|
||||
INSERT INTO waf_configs
|
||||
(domain_id, enabled, mode, paranoia_level,
|
||||
rule_exclusions, exclusion_notes, trusted_proxies, custom_rules, updated_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||
rule_exclusions, crs_plugins, app_profiles, exclusion_notes, trusted_proxies, custom_rules, updated_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
|
||||
ON CONFLICT (domain_id) DO UPDATE SET
|
||||
enabled = EXCLUDED.enabled,
|
||||
mode = EXCLUDED.mode,
|
||||
paranoia_level = EXCLUDED.paranoia_level,
|
||||
rule_exclusions = EXCLUDED.rule_exclusions,
|
||||
crs_plugins = EXCLUDED.crs_plugins,
|
||||
app_profiles = EXCLUDED.app_profiles,
|
||||
exclusion_notes = EXCLUDED.exclusion_notes,
|
||||
trusted_proxies = EXCLUDED.trusted_proxies,
|
||||
custom_rules = EXCLUDED.custom_rules,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING id, domain_id, enabled, mode, paranoia_level,
|
||||
rule_exclusions, exclusion_notes, trusted_proxies, custom_rules, updated_at
|
||||
rule_exclusions, crs_plugins, app_profiles, exclusion_notes, trusted_proxies, custom_rules, updated_at
|
||||
`,
|
||||
c.DomainID, c.Enabled, c.Mode, c.ParanoiaLevel,
|
||||
c.RuleExclusions, c.ExclusionNotes, c.TrustedProxies, c.CustomRules, c.UpdatedAt,
|
||||
c.RuleExclusions, c.CRSPlugins, c.AppProfiles, c.ExclusionNotes, c.TrustedProxies, c.CustomRules, c.UpdatedAt,
|
||||
)
|
||||
return scan(row)
|
||||
}
|
||||
@@ -200,11 +202,23 @@ type DomainConfigPair struct {
|
||||
|
||||
// ListAllWithDomain returns all WAF configs joined with their domain name.
|
||||
// Used by the WAF agent to build the hostname→engine mapping.
|
||||
//
|
||||
// Wichtig: crs_plugins UND app_profiles werden hier geladen — früher fehlte
|
||||
// crs_plugins, dadurch waren die gewählten Built-in-CRS-Plugins im laufenden
|
||||
// Agent nie aktiv. app_profiles (benutzerdefinierte Rule-ID-Bündel) werden hier
|
||||
// in die effektiven rule_exclusions der Domain gemischt und ihr updated_at
|
||||
// fließt in das effektive updated_at ein — so baut der Manager die Engine neu,
|
||||
// sobald ein Profil bearbeitet wird (der Rebuild-Trigger hängt an updated_at).
|
||||
func (r *Repo) ListAllWithDomain(ctx context.Context) ([]DomainConfigPair, error) {
|
||||
profiles, err := r.profilesByName(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := r.Pool.Query(ctx, `
|
||||
SELECT d.name,
|
||||
w.id, w.domain_id, w.enabled, w.mode, w.paranoia_level,
|
||||
w.rule_exclusions, w.trusted_proxies, w.custom_rules, w.updated_at
|
||||
w.rule_exclusions, w.crs_plugins, w.app_profiles,
|
||||
w.trusted_proxies, w.custom_rules, w.updated_at
|
||||
FROM waf_configs w
|
||||
JOIN domains d ON d.id = w.domain_id
|
||||
WHERE d.active = true
|
||||
@@ -221,12 +235,49 @@ func (r *Repo) ListAllWithDomain(ctx context.Context) ([]DomainConfigPair, error
|
||||
if err := rows.Scan(
|
||||
&p.Hostname,
|
||||
&c.ID, &c.DomainID, &c.Enabled, &c.Mode, &c.ParanoiaLevel,
|
||||
&c.RuleExclusions, &c.TrustedProxies, &c.CustomRules, &c.UpdatedAt,
|
||||
&c.RuleExclusions, &c.CRSPlugins, &c.AppProfiles,
|
||||
&c.TrustedProxies, &c.CustomRules, &c.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.RuleExclusions, c.UpdatedAt = mergeProfileExclusions(c.RuleExclusions, c.UpdatedAt, c.AppProfiles, profiles)
|
||||
p.Config = c
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// mergeProfileExclusions vereint die domain-eigenen Ausnahmen mit denen aller
|
||||
// zugewiesenen App-Profile (dedupliziert, stabile Reihenfolge) und hebt das
|
||||
// effektive updated_at auf das Maximum aus Config + zugewiesenen Profilen an.
|
||||
// Pure Funktion (leicht testbar, keine DB).
|
||||
func mergeProfileExclusions(own []string, updatedAt time.Time, assigned []string, profiles map[string]models.WafAppProfile) ([]string, time.Time) {
|
||||
if len(assigned) == 0 {
|
||||
return own, updatedAt
|
||||
}
|
||||
seen := make(map[string]struct{}, len(own))
|
||||
merged := make([]string, 0, len(own))
|
||||
for _, id := range own {
|
||||
if _, ok := seen[id]; !ok {
|
||||
seen[id] = struct{}{}
|
||||
merged = append(merged, id)
|
||||
}
|
||||
}
|
||||
effUpdated := updatedAt
|
||||
for _, name := range assigned {
|
||||
prof, ok := profiles[name]
|
||||
if !ok {
|
||||
continue // unbekanntes/gelöschtes Profil defensiv ignorieren
|
||||
}
|
||||
if prof.UpdatedAt.After(effUpdated) {
|
||||
effUpdated = prof.UpdatedAt
|
||||
}
|
||||
for _, id := range prof.RuleExclusions {
|
||||
if _, ok := seen[id]; !ok {
|
||||
seen[id] = struct{}{}
|
||||
merged = append(merged, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged, effUpdated
|
||||
}
|
||||
|
||||
@@ -48,12 +48,23 @@ func buildDirectives(cfg models.WafConfig, crsDir string) string {
|
||||
pl = 1
|
||||
}
|
||||
fmt.Fprintf(&sb, "SecAction \"id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=%d\"\n", pl)
|
||||
setupConf := filepath.Join(crsDir, "crs-setup.conf")
|
||||
if _, err := os.Stat(setupConf); err == nil {
|
||||
fmt.Fprintf(&sb, "Include %s\n", setupConf)
|
||||
includeIfExists(&sb, filepath.Join(crsDir, "crs-setup.conf"))
|
||||
|
||||
// CRS-App-Exclusion-Plugins: config + before laufen VOR den CRS-Rules
|
||||
// (setzen Enable-Vars + pfad-scoped ctl:ruleRemoveById), after DANACH —
|
||||
// exakt nach OWASP-CRS-Plugin-Spec. Es werden NUR die für DIESE Domain
|
||||
// gewählten Plugins inkludiert (per-Domain, nicht global).
|
||||
plugins := resolveCRSPlugins(cfg.CRSPlugins)
|
||||
for _, prefix := range plugins {
|
||||
includeIfExists(&sb, filepath.Join(crsDir, "plugins", prefix+"-config.conf"))
|
||||
includeIfExists(&sb, filepath.Join(crsDir, "plugins", prefix+"-before.conf"))
|
||||
}
|
||||
// rules/*.conf ist ein Glob (kein Stat) — crsAvailable() hat oben bereits
|
||||
// bestätigt, dass mind. eine .conf existiert.
|
||||
fmt.Fprintf(&sb, "Include %s\n", filepath.Join(crsDir, "rules", "*.conf"))
|
||||
for _, prefix := range plugins {
|
||||
includeIfExists(&sb, filepath.Join(crsDir, "plugins", prefix+"-after.conf"))
|
||||
}
|
||||
rulesGlob := filepath.Join(crsDir, "rules", "*.conf")
|
||||
fmt.Fprintf(&sb, "Include %s\n", rulesGlob)
|
||||
}
|
||||
|
||||
// Rule exclusions (applied after CRS load so they override CRS).
|
||||
@@ -78,6 +89,35 @@ func buildDirectives(cfg models.WafConfig, crsDir string) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// KnownCRSPlugins mappt den kurzen Plugin-Namen (gespeichert in
|
||||
// waf_configs.crs_plugins, im UI gewählt) auf sein Datei-Prefix in
|
||||
// <crsDir>/plugins/. Nur diese werden paketiert (postinst) und akzeptiert.
|
||||
var KnownCRSPlugins = map[string]string{
|
||||
"nextcloud": "nextcloud-rule-exclusions",
|
||||
"wordpress": "wordpress-rule-exclusions",
|
||||
"drupal": "drupal-rule-exclusions",
|
||||
}
|
||||
|
||||
// resolveCRSPlugins mappt gewählte Plugin-Namen auf ihre Datei-Prefixe und
|
||||
// filtert unbekannte/leere raus — defensiv, nie ungültige Includes rendern.
|
||||
func resolveCRSPlugins(names []string) []string {
|
||||
out := make([]string, 0, len(names))
|
||||
for _, n := range names {
|
||||
if prefix, ok := KnownCRSPlugins[strings.TrimSpace(n)]; ok {
|
||||
out = append(out, prefix)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// includeIfExists rendert eine Include-Zeile nur, wenn die Datei existiert — so
|
||||
// bricht ein gewähltes-aber-nicht-installiertes Plugin die Config nicht.
|
||||
func includeIfExists(sb *strings.Builder, path string) {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
fmt.Fprintf(sb, "Include %s\n", path)
|
||||
}
|
||||
}
|
||||
|
||||
func ruleEngineMode(mode string) string {
|
||||
switch mode {
|
||||
case "blocking":
|
||||
|
||||
60
internal/waf/engine_plugins_test.go
Normal file
60
internal/waf/engine_plugins_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||
)
|
||||
|
||||
// mustWrite legt eine Datei (inkl. Verzeichnis) an.
|
||||
func mustWrite(t *testing.T, p, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDirectives_CRSPluginIncludeOrder(t *testing.T) {
|
||||
crs := t.TempDir()
|
||||
mustWrite(t, filepath.Join(crs, "crs-setup.conf"), "# setup\n")
|
||||
mustWrite(t, filepath.Join(crs, "rules", "REQUEST-942.conf"), "# rules\n")
|
||||
mustWrite(t, filepath.Join(crs, "plugins", "nextcloud-rule-exclusions-config.conf"), "# nc config\n")
|
||||
mustWrite(t, filepath.Join(crs, "plugins", "nextcloud-rule-exclusions-before.conf"), "# nc before\n")
|
||||
|
||||
cfg := models.WafConfig{Mode: "blocking", ParanoiaLevel: 1, CRSPlugins: []string{"nextcloud", "unknown-x"}}
|
||||
out := buildDirectives(cfg, crs)
|
||||
|
||||
iSetup := strings.Index(out, "crs-setup.conf")
|
||||
iCfg := strings.Index(out, "nextcloud-rule-exclusions-config.conf")
|
||||
iBefore := strings.Index(out, "nextcloud-rule-exclusions-before.conf")
|
||||
iRules := strings.Index(out, filepath.Join("rules", "*.conf"))
|
||||
if iSetup < 0 || iCfg < 0 || iBefore < 0 || iRules < 0 {
|
||||
t.Fatalf("erwartete Includes fehlen:\n%s", out)
|
||||
}
|
||||
// config + before MÜSSEN vor den CRS-Rules stehen (Plugin-Spec).
|
||||
if iSetup >= iCfg || iCfg >= iBefore || iBefore >= iRules {
|
||||
t.Errorf("falsche Include-Reihenfolge (setup=%d cfg=%d before=%d rules=%d):\n%s", iSetup, iCfg, iBefore, iRules, out)
|
||||
}
|
||||
// Unbekanntes Plugin darf NICHT inkludiert werden (Whitelist).
|
||||
if strings.Contains(out, "unknown-x") {
|
||||
t.Errorf("unbekanntes Plugin wurde inkludiert:\n%s", out)
|
||||
}
|
||||
// Nicht existente after.conf → keine Include-Zeile.
|
||||
if strings.Contains(out, "nextcloud-rule-exclusions-after.conf") {
|
||||
t.Errorf("nicht existente after.conf wurde inkludiert:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCRSPlugins(t *testing.T) {
|
||||
got := resolveCRSPlugins([]string{"nextcloud", " wordpress ", "bogus", ""})
|
||||
want := "nextcloud-rule-exclusions,wordpress-rule-exclusions"
|
||||
if strings.Join(got, ",") != want {
|
||||
t.Errorf("resolveCRSPlugins=%v want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ func (a *SPOEAgent) handle(ctx context.Context, w *encoding.ActionWriter, m *enc
|
||||
httpVer string
|
||||
host string
|
||||
rawHdrs string
|
||||
body []byte // gepufferter Request-Body (via HAProxy option http-buffer-request)
|
||||
)
|
||||
|
||||
// Iterate over the key-value pairs HAProxy sent with this message.
|
||||
@@ -63,6 +64,12 @@ func (a *SPOEAgent) handle(ctx context.Context, w *encoding.ActionWriter, m *enc
|
||||
host = string(entry.ValueBytes())
|
||||
case entry.NameEquals("headers"):
|
||||
rawHdrs = string(entry.ValueBytes())
|
||||
case entry.NameEquals("body"):
|
||||
// Kopieren: entry wird nach Reset() wiederverwendet, der
|
||||
// zugrundeliegende Puffer darf nicht referenziert bleiben.
|
||||
if b := entry.ValueBytes(); len(b) > 0 {
|
||||
body = append([]byte(nil), b...)
|
||||
}
|
||||
}
|
||||
entry.Reset()
|
||||
}
|
||||
@@ -119,6 +126,34 @@ func (a *SPOEAgent) handle(ctx context.Context, w *encoding.ActionWriter, m *enc
|
||||
// Evaluate request headers.
|
||||
interruption := tx.ProcessRequestHeaders()
|
||||
|
||||
// Request-Body inspizieren (POST/PUT-Payloads: Form-SQLi, JSON-Injection,
|
||||
// Uploads). Nur wenn die Header-Phase noch nicht geblockt hat. HAProxy
|
||||
// liefert den Body via `option http-buffer-request` (bis tune.bufsize) —
|
||||
// größere Bodies werden zur Prüfung gekappt. Content-Type kam bereits
|
||||
// über die Header, sodass Coraza urlencoded/multipart/json korrekt parst.
|
||||
if interruption == nil {
|
||||
if len(body) > 0 {
|
||||
if it, _, err := tx.WriteRequestBody(body); err != nil {
|
||||
slog.Warn("waf: WriteRequestBody", "error", err)
|
||||
} else if it != nil {
|
||||
interruption = it
|
||||
}
|
||||
}
|
||||
// ProcessRequestBody MUSS immer laufen — auch ohne Body. In Coraza wird
|
||||
// die GESAMTE Phase 2 (SQLi 942xxx, XSS 941xxx, die den Query-String/ARGS
|
||||
// prüfen) erst hier ausgewertet. Wurde das an len(body)>0 gekoppelt,
|
||||
// blieben GET-Requests ohne Body von allen Phase-2-Regeln ungeprüft →
|
||||
// Query-String-Angriffe (?id=1' OR 1=1, ?x=<script>) liefen komplett
|
||||
// durch. Der häufigste Web-Angriffsvektor war damit blind.
|
||||
if interruption == nil {
|
||||
if it, err := tx.ProcessRequestBody(); err != nil {
|
||||
slog.Warn("waf: ProcessRequestBody", "error", err)
|
||||
} else if it != nil {
|
||||
interruption = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log all matched rules (detection + blocking).
|
||||
for _, mr := range tx.MatchedRules() {
|
||||
a.sendAlert(host, clientIP, method, uri, mr, interruption != nil)
|
||||
|
||||
50
internal/waf/spoe_phase2_test.go
Normal file
50
internal/waf/spoe_phase2_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/corazawaf/coraza/v3"
|
||||
)
|
||||
|
||||
// TestPhase2RequiresProcessRequestBody nagelt die Coraza-Semantik fest, die der
|
||||
// SPOE-Bug verletzt hatte: Phase-2-Regeln (SQLi 942xxx, XSS 941xxx — sie prüfen
|
||||
// ARGS aus dem Query-String) werden ERST von ProcessRequestBody() ausgewertet.
|
||||
// Koppelt man ProcessRequestBody an len(body)>0, bleiben GET-Requests ohne Body
|
||||
// von der gesamten Phase 2 ungeprüft. Dieser Test schlägt fehl, sollte jemand
|
||||
// den Aufruf je wieder body-abhängig machen.
|
||||
func TestPhase2RequiresProcessRequestBody(t *testing.T) {
|
||||
waf, err := coraza.NewWAF(coraza.NewWAFConfig().WithDirectives(
|
||||
"SecRuleEngine DetectionOnly\n" +
|
||||
"SecRule ARGS \"@rx attackpattern\" \"id:1234,phase:2,log,msg:'phase2-arg'\"\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("NewWAF: %v", err)
|
||||
}
|
||||
tx := waf.NewTransaction()
|
||||
defer func() { tx.ProcessLogging(); _ = tx.Close() }()
|
||||
|
||||
tx.ProcessConnection("1.2.3.4", 0, "", 0)
|
||||
tx.ProcessURI("/?x=attackpattern", "GET", "HTTP/1.1") // GET, kein Body
|
||||
tx.AddRequestHeader("Host", "test")
|
||||
tx.ProcessRequestHeaders()
|
||||
|
||||
// Vor ProcessRequestBody darf die Phase-2-Regel noch NICHT gefeuert haben.
|
||||
if n := len(tx.MatchedRules()); n != 0 {
|
||||
t.Fatalf("vor ProcessRequestBody: %d Matches, erwarte 0", n)
|
||||
}
|
||||
|
||||
if _, err := tx.ProcessRequestBody(); err != nil {
|
||||
t.Fatalf("ProcessRequestBody: %v", err)
|
||||
}
|
||||
|
||||
// Jetzt MUSS die Phase-2-Regel gegen den Query-Arg gefeuert haben.
|
||||
found := false
|
||||
for _, mr := range tx.MatchedRules() {
|
||||
if mr.Rule().ID() == 1234 {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("Phase-2-Regel feuerte auch nach ProcessRequestBody nicht — " +
|
||||
"GET-Query-Args würden ungeprüft bleiben (SPOE-Bug)")
|
||||
}
|
||||
}
|
||||
@@ -5,31 +5,31 @@
|
||||
"": {
|
||||
"name": "edgeguard-management-ui",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@tanstack/react-query": "^5.95.2",
|
||||
"antd": "^6.3.4",
|
||||
"axios": "^1.13.6",
|
||||
"i18next": "^25.10.9",
|
||||
"@ant-design/icons": "^6.3.4",
|
||||
"@tanstack/react-query": "^5.102.8",
|
||||
"antd": "^6.6.3",
|
||||
"axios": "^1.20.0",
|
||||
"i18next": "^26.4.2",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^16.6.6",
|
||||
"react-router-dom": "^7.13.2",
|
||||
"zustand": "^5.0.12",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-i18next": "^17.0.13",
|
||||
"react-router-dom": "^7.18.3",
|
||||
"zustand": "^5.0.15",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.57.0",
|
||||
"vite": "^8.0.1",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^24.13.4",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"eslint": "^10.10.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.6",
|
||||
"globals": "^17.12.0",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "^8.70.0",
|
||||
"vite": "^8.3.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -42,73 +42,69 @@
|
||||
|
||||
"@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="],
|
||||
|
||||
"@ant-design/icons": ["@ant-design/icons@6.2.2", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-zlJtE7AMbG12TeYVPhtBXwNpFInNy8mjLzcIm+0BPw16/b8ODG87YJ1G37VIF5VFscdgfsf6EweAFPTobu/3iQ=="],
|
||||
"@ant-design/icons": ["@ant-design/icons@6.3.4", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.6.0", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-kDoUlppczVyCUTFthF6cHmPzMBYqNZQhu9j6EyIX/YIdM9j9CZNZoJUjzVw7/1v1VO8WwVMmPp15r/uwIAS0AQ=="],
|
||||
|
||||
"@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="],
|
||||
"@ant-design/icons-svg": ["@ant-design/icons-svg@4.6.0", "", {}, "sha512-PRomU725ABMf/lnQp5HiB7my1kjEbFY0D10N4lXYxK6TIB1gKjIVD5MRThpDaezLgw1D774J8eOeeDB0M6wHrQ=="],
|
||||
|
||||
"@ant-design/react-slick": ["@ant-design/react-slick@2.0.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "clsx": "^2.1.1", "json2mq": "^0.2.0", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||
"@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||
"@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
||||
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
|
||||
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||
"@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
"@cacheable/memory": ["@cacheable/memory@2.2.0", "", { "dependencies": { "@cacheable/utils": "^2.5.0", "@keyv/bigmap": "^1.3.1", "hookified": "^1.15.1", "keyv": "^5.6.0" } }, "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
"@cacheable/utils": ["@cacheable/utils@2.5.0", "", { "dependencies": { "hashery": "^1.5.1", "keyv": "^5.6.0" } }, "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA=="],
|
||||
|
||||
"@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="],
|
||||
|
||||
"@emotion/unitless": ["@emotion/unitless@0.7.5", "", {}, "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
|
||||
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
|
||||
"@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
|
||||
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.3", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
||||
|
||||
@@ -126,17 +122,19 @@
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
||||
"@keyv/bigmap": ["@keyv/bigmap@1.3.1", "", { "dependencies": { "hashery": "^1.4.0", "hookified": "^1.15.0" }, "peerDependencies": { "keyv": "^5.6.0" } }, "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.128.0", "", {}, "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ=="],
|
||||
"@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="],
|
||||
|
||||
"@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="],
|
||||
"@oxc-project/types": ["@oxc-project/types@0.149.0", "", {}, "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA=="],
|
||||
|
||||
"@rc-component/cascader": ["@rc-component/cascader@1.14.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ=="],
|
||||
"@rc-component/async-validator": ["@rc-component/async-validator@6.0.0", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA=="],
|
||||
|
||||
"@rc-component/cascader": ["@rc-component/cascader@1.22.0", "", { "dependencies": { "@rc-component/select": "~1.10.0", "@rc-component/tree": "~1.4.0", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-SffrA57aS9oub3VuI7ajPhJTPtaNxngSvtRhD40Rd8dwJ5vfWPSrVanWgeepdWFGBt7EHftIK5RUU0u3rCTwWw=="],
|
||||
|
||||
"@rc-component/checkbox": ["@rc-component/checkbox@2.0.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ=="],
|
||||
|
||||
@@ -144,45 +142,47 @@
|
||||
|
||||
"@rc-component/color-picker": ["@rc-component/color-picker@3.1.1", "", { "dependencies": { "@ant-design/fast-color": "^3.0.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg=="],
|
||||
|
||||
"@rc-component/context": ["@rc-component/context@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw=="],
|
||||
"@rc-component/context": ["@rc-component/context@2.0.2", "", { "dependencies": { "@rc-component/util": "^1.11.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-uiGpAlblCNlziHPwj4S4Iy/oemeuz/hR03mbiEjTCXwsqOIN3BOzsRMyDwpyO5Fm0vIEEJRUf9ZtbRLbhksuTA=="],
|
||||
|
||||
"@rc-component/dialog": ["@rc-component/dialog@1.8.4", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw=="],
|
||||
"@rc-component/dialog": ["@rc-component/dialog@1.10.0", "", { "dependencies": { "@rc-component/motion": "^1.3.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-eDukNlz9vNszAGv7i3zKXdxEd3wgVmNxuJijYt8zvTh17QwTu8KK/bdURRd/lU4qaMzhO1HKKmMrwOnkaw0BvQ=="],
|
||||
|
||||
"@rc-component/drawer": ["@rc-component/drawer@1.4.2", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.1.3", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q=="],
|
||||
|
||||
"@rc-component/dropdown": ["@rc-component/dropdown@1.0.2", "", { "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg=="],
|
||||
"@rc-component/dropdown": ["@rc-component/dropdown@1.0.3", "", { "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-YTST/N6kpqpDz3IMuM/PSSZnrDpSOA6dgHv12gPA90ZTSLv2CoqkZ0+9NtwTY6BeO7dstPblSic2QJg7dSFy/g=="],
|
||||
|
||||
"@rc-component/form": ["@rc-component/form@1.8.1", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ=="],
|
||||
"@rc-component/form": ["@rc-component/form@1.8.6", "", { "dependencies": { "@rc-component/async-validator": "^6.0.0", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-1EXVsSKPZC6kGrIqxVck7kAWM6T65b6et/n5wIcyiaIa41VrDshD0nVLHoBDEIZr2ATQsOP6icb4XQkNFMLBAw=="],
|
||||
|
||||
"@rc-component/image": ["@rc-component/image@1.9.0", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ=="],
|
||||
"@rc-component/image": ["@rc-component/image@1.10.0", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-BjeZCRQ+hw+4WAhvrw8rJvy5fckA2xpf/X2XQEOABUHvLTNB9inB98X3Mp54jYQ7g10DfWERQWHXeC4ylxp1Uw=="],
|
||||
|
||||
"@rc-component/input": ["@rc-component/input@1.1.2", "", { "dependencies": { "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg=="],
|
||||
"@rc-component/input": ["@rc-component/input@1.3.1", "", { "dependencies": { "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig=="],
|
||||
|
||||
"@rc-component/input-number": ["@rc-component/input-number@1.6.2", "", { "dependencies": { "@rc-component/mini-decimal": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w=="],
|
||||
|
||||
"@rc-component/mentions": ["@rc-component/mentions@1.6.0", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/menu": "~1.2.0", "@rc-component/textarea": "~1.1.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ=="],
|
||||
"@rc-component/listy": ["@rc-component/listy@1.2.3", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.1", "@rc-component/virtual-list": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-IXiMjV5s0rczLBlfh7G5nB4M3365mrEeedjwKtf5I+Ns3PqRUsebR2h5u8CeFarsVfLUPC2I5p0h09TNoOWyvQ=="],
|
||||
|
||||
"@rc-component/menu": ["@rc-component/menu@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg=="],
|
||||
"@rc-component/mentions": ["@rc-component/mentions@1.12.0", "", { "dependencies": { "@rc-component/input": "~1.3.1", "@rc-component/menu": "~1.5.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-v5MBx8zDcqCT+uybJgKpcx3Fgrvj0m3u+A/15RlSBaMx12lLoJZTTYggC3pbYYfohcKujWWUyBz6O6Gp2vGiNA=="],
|
||||
|
||||
"@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw=="],
|
||||
"@rc-component/menu": ["@rc-component/menu@1.5.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-RjvzPsZkwVEg3xu+/jDDqDeUCBJAnWylONFlGeQgg5enlNAnlpybqhaMGE/UPEIvr6hL90PGhjFBguJqj0Qb2Q=="],
|
||||
|
||||
"@rc-component/motion": ["@rc-component/motion@1.3.2", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ=="],
|
||||
"@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.4", "", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w=="],
|
||||
|
||||
"@rc-component/motion": ["@rc-component/motion@1.3.3", "", { "dependencies": { "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Xh3IszxvlSv3/PLYFyC2UZi9LNB83yOnkB/LNmRzaypZLvkhqUIPS7MQpGZcCMWrNsXV2p6YTSWbSGvFpEle9A=="],
|
||||
|
||||
"@rc-component/mutate-observer": ["@rc-component/mutate-observer@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w=="],
|
||||
|
||||
"@rc-component/notification": ["@rc-component/notification@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA=="],
|
||||
"@rc-component/notification": ["@rc-component/notification@2.0.8", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-MTRA3b8eHE14gh5R9nlXzbWZEDEBFVkEHj/FI1VOXw2zrSnSxqbuuU3drsq+k53e6MHwtegoFay1tnNYhtl61Q=="],
|
||||
|
||||
"@rc-component/overflow": ["@rc-component/overflow@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA=="],
|
||||
|
||||
"@rc-component/pagination": ["@rc-component/pagination@1.2.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw=="],
|
||||
"@rc-component/pagination": ["@rc-component/pagination@1.4.0", "", { "dependencies": { "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-CW1g7P9V8u+e8JQdUsl2RWg+GCsoee0mtJjZUCCxn/vb3jzOwDKm6hAdwddHCVBfWJ58eGUBZz3IvnU8rRktjw=="],
|
||||
|
||||
"@rc-component/picker": ["@rc-component/picker@1.9.1", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g=="],
|
||||
"@rc-component/picker": ["@rc-component/picker@1.12.2", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.13.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-ZB1A+tzTQkgtR4flPrAMvxIAOelIIOOOsp22fU2Nad6PbZcJacnyGJUJqUH7ZS9mwxeUwurf1pLMKXrVsK1tlA=="],
|
||||
|
||||
"@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
|
||||
"@rc-component/portal": ["@rc-component/portal@2.2.1", "", { "dependencies": { "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ck+r1kW/JSv0wxPji3KN2ss9K6Z0qqwusw/mf/0JobXhZ8hC2ejZwCJObW/SvDi0uhA0VzmCnx0CaCci95tcmA=="],
|
||||
|
||||
"@rc-component/progress": ["@rc-component/progress@1.0.2", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ=="],
|
||||
"@rc-component/progress": ["@rc-component/progress@1.0.3", "", { "dependencies": { "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Q1nPLIvKr95el/q76yaBh5nv1hqCsWaHQb8U7vXEDMefEhHgM7n3CPoN+o4LHnTyANQHmMFgoS/naAT4Bhxuaw=="],
|
||||
|
||||
"@rc-component/qrcode": ["@rc-component/qrcode@1.1.1", "", { "dependencies": { "@babel/runtime": "^7.24.7" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA=="],
|
||||
"@rc-component/qrcode": ["@rc-component/qrcode@2.0.0", "", { "dependencies": { "@babel/runtime": "^7.24.7" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aAv3QhPP1xyafuTZOxub6a54pCeBnN3IwQkpETrBtthq4BL5IgxnCbuoBWPDpdLw1y1j6BgBUCAKV92+yX06Dw=="],
|
||||
|
||||
"@rc-component/rate": ["@rc-component/rate@1.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw=="],
|
||||
|
||||
@@ -190,150 +190,138 @@
|
||||
|
||||
"@rc-component/segmented": ["@rc-component/segmented@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg=="],
|
||||
|
||||
"@rc-component/select": ["@rc-component/select@1.6.15", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g=="],
|
||||
"@rc-component/select": ["@rc-component/select@1.10.1", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.11.1", "@rc-component/virtual-list": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-H+yQsl+qED9NilQ3g6zdpsMwUgwVjrcMTkNHAWRVU/MoNCYgTbDgU+MIMgZDK+rVdd2JUfI/MkysMcZZ0cyQKw=="],
|
||||
|
||||
"@rc-component/slider": ["@rc-component/slider@1.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g=="],
|
||||
"@rc-component/slider": ["@rc-component/slider@1.1.1", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LSzgWGYDgeCDgR4r1XlU29gbYws6HpLnvJd/uMhLeW/vQgxldeR+Wb4uzHDCHiYEbr1bnEHWdjkPxjJRHxuiig=="],
|
||||
|
||||
"@rc-component/steps": ["@rc-component/steps@1.2.2", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw=="],
|
||||
"@rc-component/steps": ["@rc-component/steps@1.2.3", "", { "dependencies": { "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/b9gHcvDGjTDWJZW7+1kKFvzs0jrEeQ8mFvvIgX+h7xNT0FsOYODJYKjyJR4fPbvMBCfCOEqaBK66HBxov5BQA=="],
|
||||
|
||||
"@rc-component/switch": ["@rc-component/switch@1.0.3", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw=="],
|
||||
|
||||
"@rc-component/table": ["@rc-component/table@1.9.1", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.1.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg=="],
|
||||
"@rc-component/table": ["@rc-component/table@1.11.1", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.11.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-OWdS6DMmeWb7bJBGqPxYZpQbzBlBiXZUu2sqo6Ii7Sjs9GeK1IsrXrWk26SL2c6KEseabswdxrRj7WUm9LdECw=="],
|
||||
|
||||
"@rc-component/tabs": ["@rc-component/tabs@1.7.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w=="],
|
||||
"@rc-component/tabs": ["@rc-component/tabs@1.13.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.5.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3FXr+9ZII8tFLWuDWrgjRaxZd2mazkKJAGRFEfVmp4r1qnBglOlxQ78l54Td1aj6/iFaaEReKSAF2HToztU5uw=="],
|
||||
|
||||
"@rc-component/textarea": ["@rc-component/textarea@1.1.2", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A=="],
|
||||
"@rc-component/tooltip": ["@rc-component/tooltip@1.5.2", "", { "dependencies": { "@rc-component/trigger": "^3.10.0", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-VLq4BclU3LgRe46g2hTr2bdsM8tFYs7dVwpuD88rPNNg4lCGe8mBSS4FiRzYzS+ffnZ46y4SI8KYKo9K7qBnhw=="],
|
||||
|
||||
"@rc-component/tooltip": ["@rc-component/tooltip@1.4.0", "", { "dependencies": { "@rc-component/trigger": "^3.7.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg=="],
|
||||
"@rc-component/tour": ["@rc-component/tour@2.4.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg=="],
|
||||
|
||||
"@rc-component/tour": ["@rc-component/tour@2.3.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow=="],
|
||||
"@rc-component/tree": ["@rc-component/tree@1.4.0", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.11.1", "@rc-component/virtual-list": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-dGsJGDJQedA0BqqVgj3F8BvHXTSZijyhTXdbAdkcx8lynzZkty/CV3Z3LOm/fxz+BCfl3dfGiAQpb7Q5XNvl0Q=="],
|
||||
|
||||
"@rc-component/tree": ["@rc-component/tree@1.2.4", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.8.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w=="],
|
||||
"@rc-component/tree-select": ["@rc-component/tree-select@1.16.1", "", { "dependencies": { "@rc-component/select": "~1.10.0", "@rc-component/tree": "~1.4.0", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-a1Oi6EJhqAhdOxxupdJi6fP0RPHMKn5TcfkX2+llaQ4lF4nwfH7b6SCHcnsybaa2s+pk1yZYwVyeOYkDnEBRdg=="],
|
||||
|
||||
"@rc-component/tree-select": ["@rc-component/tree-select@1.8.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ=="],
|
||||
"@rc-component/trigger": ["@rc-component/trigger@3.10.1", "", { "dependencies": { "@rc-component/motion": "^1.3.3", "@rc-component/portal": "^2.2.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-mXlDN0IXdtV8Yqqm8195ECCyrbmfvvfKvwVvSlH0+qvKD6BUF8gRhEjSy0FOcD1+CcDRHgTiX99LoxfQrmh3Cw=="],
|
||||
|
||||
"@rc-component/trigger": ["@rc-component/trigger@3.9.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.2.0", "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg=="],
|
||||
"@rc-component/upload": ["@rc-component/upload@1.1.1", "", { "dependencies": { "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-GvYWSKeaJTOxxC5p6+nOSadzfvXA1h8C/iHFPFZX+szH3JUXrvs+DLiW8YUTBgvMh8m63mJeHrlYlJzAlg+pDA=="],
|
||||
|
||||
"@rc-component/upload": ["@rc-component/upload@1.1.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw=="],
|
||||
"@rc-component/util": ["@rc-component/util@1.13.0", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^19.2.7" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-IXf2zBfZrbQGT+G8LxnoICDBGnMHIPIiq5HCRn3EDfKUx28x1rnbZBj46ULJf4sM8ImTG1SepqfNBdxZziqQ4w=="],
|
||||
|
||||
"@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
|
||||
"@rc-component/virtual-list": ["@rc-component/virtual-list@1.5.1", "", { "dependencies": { "@babel/runtime": "^8.0.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-boqHxdtyWC88u8quYgEO49bcBy5fzRiOcnBge+N4nLzs2k8hUQ/yw7JE9dM6yCBE4jSm5YSHVCVMS+suBuJGKA=="],
|
||||
|
||||
"@rc-component/virtual-list": ["@rc-component/virtual-list@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ=="],
|
||||
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.8", "", { "os": "android", "cpu": "arm" }, "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.18", "", { "os": "android", "cpu": "arm64" }, "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ=="],
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.8", "", { "os": "android", "cpu": "arm64" }, "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ=="],
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g=="],
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw=="],
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm" }, "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg=="],
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.8", "", { "os": "linux", "cpu": "arm" }, "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ=="],
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug=="],
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg=="],
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "s390x" }, "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA=="],
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.18", "", { "os": "linux", "cpu": "x64" }, "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw=="],
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.18", "", { "os": "linux", "cpu": "x64" }, "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA=="],
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.18", "", { "os": "none", "cpu": "arm64" }, "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A=="],
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.8", "", { "os": "none", "cpu": "arm64" }, "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.18", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg=="],
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ=="],
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.8", "", { "os": "win32", "cpu": "x64" }, "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.18", "", { "os": "win32", "cpu": "x64" }, "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.102.8", "", {}, "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.100.9", "", {}, "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ=="],
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.102.8", "", { "dependencies": { "@tanstack/query-core": "5.102.8" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.100.9", "", { "dependencies": { "@tanstack/query-core": "5.100.9" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.12.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-8oljBDGun9cIsZRJR6fkihn0TSXJI0UDOOhncYaERq6M0JMDoPLxyscwruJcb4GKS6dvK/d8xebYBg27h/duaQ=="],
|
||||
"@types/node": ["@types/node@24.13.4", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
"@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
"@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="],
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.70.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.70.0", "@typescript-eslint/type-utils": "8.70.0", "@typescript-eslint/utils": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.70.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="],
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.70.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.70.0", "@typescript-eslint/types": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="],
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.70.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.70.0", "@typescript-eslint/types": "^8.70.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="],
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.70.0", "", { "dependencies": { "@typescript-eslint/types": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0" } }, "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="],
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.70.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="],
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.70.0", "", { "dependencies": { "@typescript-eslint/types": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0", "@typescript-eslint/utils": "8.70.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="],
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.70.0", "", {}, "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="],
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.70.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.70.0", "@typescript-eslint/tsconfig-utils": "8.70.0", "@typescript-eslint/types": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="],
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.70.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.70.0", "@typescript-eslint/types": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.70.0", "", { "dependencies": { "@typescript-eslint/types": "8.70.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"antd": ["antd@6.3.7", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.1.1", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.28.4", "@rc-component/cascader": "~1.14.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.8.4", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.8.1", "@rc-component/image": "~1.9.0", "@rc-component/input": "~1.1.2", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~1.2.0", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.9.1", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.9.1", "@rc-component/tabs": "~1.7.0", "@rc-component/textarea": "~1.1.2", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.3.0", "@rc-component/tree": "~1.2.4", "@rc-component/tree-select": "~1.8.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-WTHi4bHVNKpYXLHESzU0Tts7rRNQeL84Bph9dfI3Qw7mHbTulExDcYKNHny5CTXcrBBOpraXbU9miBAwUR5vaw=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
"antd": ["antd@6.6.3", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.3.4", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.29.2", "@rc-component/cascader": "~1.22.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.10.0", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.3", "@rc-component/form": "~1.8.6", "@rc-component/image": "~1.10.0", "@rc-component/input": "~1.3.1", "@rc-component/input-number": "~1.6.2", "@rc-component/listy": "~1.2.3", "@rc-component/mentions": "~1.12.0", "@rc-component/menu": "~1.5.0", "@rc-component/motion": "^1.3.3", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~2.0.8", "@rc-component/pagination": "~1.4.0", "@rc-component/picker": "~1.12.2", "@rc-component/progress": "~1.0.3", "@rc-component/qrcode": "~2.0.0", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.10.1", "@rc-component/slider": "~1.1.1", "@rc-component/steps": "~1.2.3", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.11.1", "@rc-component/tabs": "~1.13.0", "@rc-component/tooltip": "~1.5.2", "@rc-component/tour": "~2.4.0", "@rc-component/tree": "~1.4.0", "@rc-component/tree-select": "~1.16.1", "@rc-component/trigger": "^3.10.1", "@rc-component/upload": "~1.1.1", "@rc-component/util": "^1.13.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-USnOxcOhWEQc65pHapSiCrZOijWef783XnqYZv8fuYIcYVoFxNxl6TsnsUhmoJyZheD/XwAx9CyO+hlyekDCQA=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"axios": ["axios@1.16.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w=="],
|
||||
"axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.28", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.22", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
|
||||
"brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||
"browserslist": ["browserslist@4.28.9", "", { "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.420", "node-releases": "^2.0.54", "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" } }, "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg=="],
|
||||
|
||||
"cacheable": ["cacheable@2.5.0", "", { "dependencies": { "@cacheable/memory": "^2.2.0", "@cacheable/utils": "^2.5.0", "hookified": "^1.15.0", "keyv": "^5.6.0", "qified": "^0.10.1" } }, "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
@@ -342,7 +330,7 @@
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
|
||||
"dayjs": ["dayjs@1.11.23", "", {}, "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
@@ -354,13 +342,13 @@
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.353", "", {}, "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.427", "", {}, "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
@@ -368,17 +356,17 @@
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
|
||||
"eslint": ["eslint@10.10.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "11.1.5 || >11.1.6 <12", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw=="],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
|
||||
|
||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.2", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA=="],
|
||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.6", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-uZnh24On2bk478AkaDAKcpRiYhS3qFSaSNvpXxV6/Ek/BBDICkWGG7MnBSfjnIlCVBei5vsLlIeQYxbF22+Udg=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
|
||||
@@ -396,17 +384,17 @@
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
"file-entry-cache": ["file-entry-cache@11.1.5", "", { "dependencies": { "flat-cache": "^6.1.23" } }, "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
"flat-cache": ["flat-cache@6.1.23", "", { "dependencies": { "cacheable": "^2.5.0", "flatted": "^3.4.2", "hookified": "^1.15.0" } }, "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA=="],
|
||||
|
||||
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||
"flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
|
||||
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
@@ -420,32 +408,34 @@
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@17.6.0", "", {}, "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA=="],
|
||||
"globals": ["globals@17.12.0", "", {}, "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
|
||||
"hashery": ["hashery@1.5.1", "", { "dependencies": { "hookified": "^1.15.0" } }, "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
|
||||
|
||||
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
||||
|
||||
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
|
||||
"hookified": ["hookified@1.15.1", "", {}, "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg=="],
|
||||
|
||||
"i18next": ["i18next@25.10.10", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ=="],
|
||||
"html-parse-stringify": ["html-parse-stringify@4.0.1", "", {}, "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||
|
||||
"i18next": ["i18next@26.4.2", "", { "peerDependencies": { "typescript": "^5 || ^6 || ^7" }, "optionalPeers": ["typescript"] }, "sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug=="],
|
||||
|
||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
@@ -458,12 +448,8 @@
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
@@ -472,38 +458,36 @@
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
"keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
@@ -512,15 +496,15 @@
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
||||
"nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="],
|
||||
"node-releases": ["node-releases@2.0.55", "", {}, "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
@@ -528,17 +512,15 @@
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
|
||||
"postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
@@ -546,23 +528,23 @@
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
|
||||
"qified": ["qified@0.10.1", "", { "dependencies": { "hookified": "^2.1.1" } }, "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
||||
"react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
|
||||
|
||||
"react-i18next": ["react-i18next@16.6.6", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.10.9", "react": ">= 16.8.0", "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw=="],
|
||||
"react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
|
||||
|
||||
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
"react-i18next": ["react-i18next@17.0.13", "", { "dependencies": { "@babel/runtime": "^7.29.7", "html-parse-stringify": "^4.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.2.0", "react": ">= 16.8.0", "react-dom": "*", "react-native": "*", "typescript": "^5 || ^6 || ^7" }, "optionalPeers": ["react-dom", "react-native", "typescript"] }, "sha512-Cc1PscmblIHA1kljTqDwrcVMI21ydgmUzw0UAeQBe7pAOgfuRLfzXze4EUBQoeDiICzFIXXhHFoZxuetNg5D0Q=="],
|
||||
|
||||
"react-router": ["react-router@7.15.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ=="],
|
||||
"react-is": ["react-is@19.3.0", "", {}, "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA=="],
|
||||
|
||||
"react-router-dom": ["react-router-dom@7.15.0", "", { "dependencies": { "react-router": "7.15.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ=="],
|
||||
"react-router": ["react-router@7.18.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
"react-router-dom": ["react-router-dom@7.18.3", "", { "dependencies": { "react-router": "7.18.3" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-rc.18", "", { "dependencies": { "@oxc-project/types": "=0.128.0", "@rolldown/pluginutils": "1.0.0-rc.18" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-x64": "1.0.0-rc.18", "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg=="],
|
||||
"rolldown": ["rolldown@1.2.8", "", { "dependencies": { "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.8", "@rolldown/binding-android-arm64": "1.2.8", "@rolldown/binding-darwin-arm64": "1.2.8", "@rolldown/binding-darwin-x64": "1.2.8", "@rolldown/binding-freebsd-x64": "1.2.8", "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", "@rolldown/binding-linux-arm64-gnu": "1.2.8", "@rolldown/binding-linux-arm64-musl": "1.2.8", "@rolldown/binding-linux-ppc64-gnu": "1.2.8", "@rolldown/binding-linux-s390x-gnu": "1.2.8", "@rolldown/binding-linux-x64-gnu": "1.2.8", "@rolldown/binding-linux-x64-musl": "1.2.8", "@rolldown/binding-openharmony-arm64": "1.2.8", "@rolldown/binding-win32-arm64-msvc": "1.2.8", "@rolldown/binding-win32-x64-msvc": "1.2.8" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
"scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
|
||||
|
||||
"scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
|
||||
|
||||
@@ -578,37 +560,29 @@
|
||||
|
||||
"string-convert": ["string-convert@0.2.1", "", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"throttle-debounce": ["throttle-debounce@5.0.2", "", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="],
|
||||
"typescript-eslint": ["typescript-eslint@8.70.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.70.0", "@typescript-eslint/parser": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0", "@typescript-eslint/utils": "8.70.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
"update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
"use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="],
|
||||
|
||||
"vite": ["vite@8.0.11", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0-rc.18", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow=="],
|
||||
|
||||
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
|
||||
"vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
@@ -618,28 +592,20 @@
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||
"zod": ["zod@4.6.2", "", {}, "sha512-lh5RCAGFa1Cm2hjtNwLQhSs/AsqdWnTQaBER9fEwN/88pSh7KOtJavtBx/0VlkN/uFd61SwYmljLMDAsHlvzBQ=="],
|
||||
|
||||
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
||||
|
||||
"zustand": ["zustand@5.0.13", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ=="],
|
||||
"zustand": ["zustand@5.0.15", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
"@rc-component/virtual-list/@babel/runtime": ["@babel/runtime@8.0.5", "", {}, "sha512-7NK+Lz3spQ52XsUGTxIEVU4jYN2/dIaX8sTxRFAUtYmttYZnVh3aehiihv+/Gb7+duMNHl2OrFcBQRyOHScxpg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.9", "", {}, "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.18", "", {}, "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
"qified/hookified": ["hookified@2.2.0", "", {}, "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA=="],
|
||||
}
|
||||
}
|
||||
|
||||
8
management-ui/go.mod
Normal file
8
management-ui/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
// Modul-Grenze, kein eigenständiges Go-Modul mit Code. Verhindert dass
|
||||
// `go vet/build/test ./...` im Root-Modul in management-ui/node_modules/
|
||||
// hineinläuft — npm-Pakete (z.B. flatted) shippen dort teils rohe .go-
|
||||
// Dateien ohne eigenes go.mod, die sich bei jedem bun/npm install ändern
|
||||
// können und sonst ungefragt in EdgeGuards Go-Quality-Gates landen.
|
||||
module git.netcell-it.de/projekte/edgeguard-native/management-ui
|
||||
|
||||
go 1.26
|
||||
@@ -10,30 +10,30 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@tanstack/react-query": "^5.95.2",
|
||||
"antd": "^6.3.4",
|
||||
"axios": "^1.13.6",
|
||||
"i18next": "^25.10.9",
|
||||
"@ant-design/icons": "^6.3.4",
|
||||
"@tanstack/react-query": "^5.102.8",
|
||||
"antd": "^6.6.3",
|
||||
"axios": "^1.20.0",
|
||||
"i18next": "^26.4.2",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^16.6.6",
|
||||
"react-router-dom": "^7.13.2",
|
||||
"zustand": "^5.0.12"
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-i18next": "^17.0.13",
|
||||
"react-router-dom": "^7.18.3",
|
||||
"zustand": "^5.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.57.0",
|
||||
"vite": "^8.0.1"
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^24.13.4",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"eslint": "^10.10.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.6",
|
||||
"globals": "^17.12.0",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "^8.70.0",
|
||||
"vite": "^8.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,6 +454,8 @@
|
||||
"summaryCritical": "{{critical}} kritisch"
|
||||
},
|
||||
"downBackendsAlert": "{{count}} Backend(s) komplett ausgefallen — kein Server UP",
|
||||
"backupNodeBackends": "Standby-Node — Backend-Health lokal nicht aussagekräftig",
|
||||
"backupNodeBackendsDesc": "Dieser Node ist gerade keepalived-BACKUP und hält die VLAN-Gateway-VIPs nicht, kann die Backend-Subnetze also nicht erreichen — die lokalen Health-Checks laufen deshalb alle auf Timeout. Kein Ausfall: der Master-Node bedient den Traffic. Nach einem Failover (VIP übernimmt dieser Node) werden die Backends hier UP.",
|
||||
"maintenanceAlert": "{{count}} Domain(s) im Wartungs-Modus",
|
||||
"onboardingTitle": "Willkommen bei EdgeGuard",
|
||||
"onboardingIntro": "Frische Box — hier die nächsten Schritte um Customer-Traffic zu routen:",
|
||||
@@ -565,7 +567,9 @@
|
||||
"routingRulesHint": "Pfad-Präfix → Backend-Zuordnungen für diese Domain. Niedrigste Prioritätszahl gewinnt; nicht gematchte Anfragen gehen an das Primary-Backend.",
|
||||
"routingRulesEmpty": "Keine Routing-Regeln — alle Anfragen gehen an das Primary-Backend.",
|
||||
"backendUp": "Backend UP",
|
||||
"backendDown": "Backend DOWN"
|
||||
"backendDown": "Backend DOWN",
|
||||
"crowdsecTrusted": "Vertrauenswürdiges Admin-Panel (CrowdSec-Ausnahme)",
|
||||
"crowdsecTrustedHint": "Für Admin-Oberflächen (SPA), die beim Bedienen viele /api/-Requests feuern. Nimmt diesen Host von der CrowdSec-Crawl-Erkennung aus, damit du nicht fälschlich gebannt wirst. Die WAF schützt die Domain weiterhin."
|
||||
},
|
||||
"backends": {
|
||||
"title": "Backends",
|
||||
@@ -592,6 +596,8 @@
|
||||
"websocketHint": "An: erlaubt langlebige WebSocket-/Long-Poll-Verbindungen (z. B. Proxmox-Console, SSH-WS, AsyncAPI) — Tunnel-Idle 1h statt 60s. Aus: strikte HTTP-Timeouts.",
|
||||
"forceHttp1": "HTTP/1.1 erzwingen",
|
||||
"forceHttp1Hint": "Deaktiviert HTTP/2 (h2) auf der Backend-Verbindung — HAProxy handelt nur noch HTTP/1.1 aus. Nötig für Backends die kein h2 unterstützen (z. B. ältere nginx-Konfigurationen ohne h2-Modul, Legacy-Apps).",
|
||||
"serverTimeout": "Antwort-Timeout (timeout server)",
|
||||
"serverTimeoutHint": "Wie lange HAProxy auf die Antwort dieses Backends wartet, bevor es abbricht. Leer = Default (60s). Höher setzen für langsame Upstreams die NICHT streamen (z. B. KI-/Inferenz-Server mit gepufferter Antwort). Achtung: gilt als Inaktivitäts-Timeout — streamende Backends (SSE/chunked) brauchen das meist nicht.",
|
||||
"servers": "Server",
|
||||
"noServers": "kein Server",
|
||||
"noServersWarning": "Ein oder mehrere aktive Backends haben keine Server konfiguriert.",
|
||||
@@ -919,6 +925,17 @@
|
||||
"autoUpdateHint": "Whitelist umfasst nur edgeguard, edgeguard-api, edgeguard-ui. Andere Pakete bleiben unter manueller Kontrolle. Verlangt unattended-upgrades (Distro-Standard auf Trixie). Conf-File: /etc/apt/apt.conf.d/52edgeguard-auto-updates.",
|
||||
"autoUpdateToggled": "Auto-Update-Einstellung gespeichert.",
|
||||
"autoUpdateFailed": "Auto-Update-Toggle fehlgeschlagen",
|
||||
"updateChannelCardTitle": "Update-Kanal",
|
||||
"updateChannelStable": "Stable",
|
||||
"updateChannelTesting": "Testing",
|
||||
"updateChannelApply": "Anwenden",
|
||||
"updateChannelConfirmTitle": "Update-Kanal wechseln?",
|
||||
"updateChannelSwitchTestingWarn": "Testing kann instabile Zwischenstände enthalten. Beide Nodes werden umgestellt.",
|
||||
"updateChannelSwitchStableWarn": "Wechsel zurück nach Stable kann ein Downgrade auf beiden Nodes auslösen (Testing-Versionen sind neuer datiert).",
|
||||
"updateChannelSaved": "Update-Kanal gespeichert (beide Nodes).",
|
||||
"updateChannelFailed": "Update-Kanal-Wechsel fehlgeschlagen",
|
||||
"updateChannelDrift": "Kanal-Drift zum Peer-Node erkannt (Peer: {{peer}}) — beim nächsten Wechsel wird synchronisiert.",
|
||||
"updateChannelHint": "Testing zieht datumsbasierte Zwischenversionen aus dem Testing-Repo, Stable die kuratierten Releases. Kanal gilt für beide HA-Nodes synchron.",
|
||||
"ipv6CardTitle": "IPv6",
|
||||
"ipv6On": "Aktiviert — HAProxy bindet zusätzlich zu IPv4 auf [::]:80, [::]:443 und [::]:3443.",
|
||||
"ipv6Off": "Deaktiviert — HAProxy lauscht nur auf IPv4.",
|
||||
@@ -1503,6 +1520,12 @@
|
||||
"emptyEventsDesc": "Triggers (Cert-Expiry, Backup-Fail, Cluster-Drift, License-Invalid, etc.) haben noch keinen Event gefeuert. Wenn sie feuern, landen sie hier und werden an die konfigurierten Channels zugestellt.",
|
||||
"noChannels": "kein Channel aktiv",
|
||||
"confirmDelete": "Channel {{name}} wirklich löschen?",
|
||||
"acknowledge": "Quittieren",
|
||||
"acknowledgeAll": "Alle quittieren",
|
||||
"acknowledged": "Quittiert",
|
||||
"acked": "quittiert",
|
||||
"open": "offen",
|
||||
"confirmDeleteEvents": "{{n}} Event(s) endgültig löschen?",
|
||||
"kindWebhook": "Webhook (Slack/Discord/Teams/HTTP-Endpoint)",
|
||||
"kindEmail": "E-Mail (SMTP)",
|
||||
"smtp": {
|
||||
@@ -1523,7 +1546,8 @@
|
||||
"time": "Zeit",
|
||||
"severity": "Severity",
|
||||
"subject": "Betreff",
|
||||
"delivered": "Gesendet"
|
||||
"delivered": "Gesendet",
|
||||
"status": "Status"
|
||||
}
|
||||
},
|
||||
"remotes": {
|
||||
@@ -1869,7 +1893,11 @@
|
||||
"deleteFailed": "Löschen fehlgeschlagen",
|
||||
"secretSet": "Gespeichert — leer lassen, um es unverändert zu lassen.",
|
||||
"secretUnset": "Noch nichts gespeichert.",
|
||||
"tabs": { "settings": "Einstellungen", "clients": "Clients (NAS)", "users": "Benutzer" },
|
||||
"tabs": {
|
||||
"settings": "Einstellungen",
|
||||
"clients": "Clients (NAS)",
|
||||
"users": "Benutzer"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": "RADIUS auf dieser Node aktiv",
|
||||
"listen": "Listen-Adressen"
|
||||
@@ -1923,6 +1951,9 @@
|
||||
"enabled": "Aktiviert",
|
||||
"mode": "Modus",
|
||||
"paranoia": "Paranoia-Level",
|
||||
"crsPlugins": "App-Profile (CRS-Plugins)",
|
||||
"crsPluginsHint": "Offizielle OWASP-CRS-Exclusion-Plugins für bekannte Apps — deaktivieren automatisch die typischen False-Positive-Regeln pfad-genau (z.B. Nextcloud-WebDAV, WordPress-Editor). Sauberer als manuelle Regel-IDs.",
|
||||
"crsPluginsPlaceholder": "App-Profile wählen (optional)",
|
||||
"exclusions": "Regel-Ausnahmen",
|
||||
"exclusionsHint": "Kommagetrennte Regel-IDs die deaktiviert werden (z.B. 920350, 941130).",
|
||||
"trustedProxies": "Vertrauenswürdige Proxys",
|
||||
@@ -1933,11 +1964,19 @@
|
||||
"saveFailed": "WAF-Konfiguration konnte nicht gespeichert werden.",
|
||||
"noExclusions": "Noch keine Regelausnahmen.",
|
||||
"noNote": "Keine Notiz",
|
||||
"exclusionsAddHint": "Ausnahmen über den Alarme-Tab hinzufügen — \"Als Ausnahme\" auf einem Alarm klicken."
|
||||
"exclusionsAddHint": "Regel oben suchen und hinzufügen — oder im Alarme-Tab per \"Als Ausnahme\" auf einem Alarm.",
|
||||
"exclusionAddPlaceholder": "Regel suchen (ID oder Beschreibung)…",
|
||||
"exclusionAddNote": "Notiz (optional)",
|
||||
"exclusionAddNotFound": "Keine Regel gefunden",
|
||||
"exclusionAddBtn": "Hinzufügen",
|
||||
"appProfiles": "Eigene App-Profile",
|
||||
"appProfilesHint": "Wiederverwendbare, selbst gepflegte Profile (Regel-ID-Bündel) — zentral unter „App-Profile“ anlegen und hier pro Domain zuweisen. Ergänzt die OWASP-Plugins oben.",
|
||||
"appProfilesPlaceholder": "Eigene Profile wählen (optional)"
|
||||
},
|
||||
"tabs": {
|
||||
"domains": "Domains",
|
||||
"alerts": "Alarme"
|
||||
"alerts": "Alarme",
|
||||
"profiles": "App-Profile"
|
||||
},
|
||||
"alerts": {
|
||||
"total": "Einträge",
|
||||
@@ -1966,6 +2005,39 @@
|
||||
"exceptionModalHint": "Optional: Begründung warum diese Regel ein False Positive für diese Domain ist.",
|
||||
"exceptionNotePlaceholder": "z.B. Unsere API verwendet nicht-standardisierte Header die diese Regel auslösen.",
|
||||
"alreadyExcluded": "Bereits Ausnahme"
|
||||
},
|
||||
"profiles": {
|
||||
"intro": "Wiederverwendbare Ausnahme-Profile (Bündel von CRS-Regel-IDs). Einmal anlegen, pro Domain zuweisen. Die eingebauten OWASP-Plugins sind read-only und werden pro Domain gewählt.",
|
||||
"new": "Neues Profil",
|
||||
"empty": "Noch keine eigenen Profile. Lege eins an, um Regel-Ausnahmen wiederzuverwenden.",
|
||||
"builtinInfo": "Eingebaute OWASP-CRS-Plugins (dateibasiert, gepflegt). Read-only — pro Domain im WAF-Drawer wählbar. „Als Vorlage“ erstellt daraus ein leeres eigenes Profil zum Befüllen.",
|
||||
"typeBuiltin": "OWASP",
|
||||
"typeCustom": "Eigen",
|
||||
"col": {
|
||||
"name": "Name",
|
||||
"type": "Typ",
|
||||
"description": "Beschreibung",
|
||||
"rules": "Ausnahmen"
|
||||
},
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"clone": "Klonen",
|
||||
"asTemplate": "Als Vorlage",
|
||||
"deleteConfirm": "Profil wirklich löschen? Zuweisungen an Domains verlieren dann diese Ausnahmen.",
|
||||
"createTitle": "Neues App-Profil",
|
||||
"editTitle": "App-Profil bearbeiten",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "z.B. Meine WebApp",
|
||||
"description": "Beschreibung",
|
||||
"descriptionPlaceholder": "Wofür ist dieses Profil? (optional)",
|
||||
"rules": "Regel-Ausnahmen",
|
||||
"rulesHint": "CRS-Regel-IDs die für zugewiesene Domains deaktiviert werden. Durchsuchbar nach ID oder Beschreibung.",
|
||||
"rulesPlaceholder": "Regel-IDs suchen und hinzufügen…",
|
||||
"saved": "Profil gespeichert.",
|
||||
"saveFailed": "Profil konnte nicht gespeichert werden.",
|
||||
"deleted": "Profil gelöscht.",
|
||||
"deleteFailed": "Profil konnte nicht gelöscht werden.",
|
||||
"cloneSuffix": "Kopie"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,6 +454,8 @@
|
||||
"summaryCritical": "{{critical}} critical"
|
||||
},
|
||||
"downBackendsAlert": "{{count}} backend(s) completely down — no server UP",
|
||||
"backupNodeBackends": "Standby node — local backend health is not meaningful",
|
||||
"backupNodeBackendsDesc": "This node is currently the keepalived BACKUP and does not hold the VLAN gateway VIPs, so it cannot reach the backend subnets — local health checks all time out. This is not an outage: the master node serves the traffic. After a failover (this node takes over the VIP), the backends will show UP here.",
|
||||
"maintenanceAlert": "{{count}} domain(s) in maintenance mode",
|
||||
"onboardingTitle": "Welcome to EdgeGuard",
|
||||
"onboardingIntro": "Fresh box — here are the next steps to route customer traffic:",
|
||||
@@ -565,7 +567,9 @@
|
||||
"routingRulesHint": "Path-prefix → backend mappings for this domain. Lowest priority number wins; unmatched requests go to the primary backend.",
|
||||
"routingRulesEmpty": "No routing rules — all requests go to the primary backend.",
|
||||
"backendUp": "backend UP",
|
||||
"backendDown": "backend DOWN"
|
||||
"backendDown": "backend DOWN",
|
||||
"crowdsecTrusted": "Trusted admin panel (CrowdSec exemption)",
|
||||
"crowdsecTrustedHint": "For admin UIs (SPAs) that fire many /api/ requests while you use them. Exempts this host from CrowdSec crawl detection so you don't get falsely banned. The WAF still protects the domain."
|
||||
},
|
||||
"backends": {
|
||||
"title": "Backends",
|
||||
@@ -592,6 +596,8 @@
|
||||
"websocketHint": "On: allow long-lived WebSocket / long-poll connections (Proxmox console, SSH-over-WS, AsyncAPI) — tunnel idle 1h instead of 60s. Off: strict HTTP timeouts.",
|
||||
"forceHttp1": "Force HTTP/1.1",
|
||||
"forceHttp1Hint": "Disables HTTP/2 (h2) on the backend connection — HAProxy negotiates HTTP/1.1 only. Required for backends that don't support h2 (e.g. older nginx configs without the h2 module, legacy apps).",
|
||||
"serverTimeout": "Response timeout (timeout server)",
|
||||
"serverTimeoutHint": "How long HAProxy waits for this backend's response before aborting. Empty = default (60s). Raise it for slow upstreams that do NOT stream (e.g. AI/inference servers with a buffered response). Note: this is an inactivity timeout — streaming backends (SSE/chunked) usually don't need it.",
|
||||
"servers": "Servers",
|
||||
"noServers": "no server",
|
||||
"noServersWarning": "One or more active backends have no servers configured.",
|
||||
@@ -919,6 +925,17 @@
|
||||
"autoUpdateHint": "Whitelist covers edgeguard, edgeguard-api, edgeguard-ui only. Other packages stay under manual control. Requires unattended-upgrades (Trixie distro default). Conf file: /etc/apt/apt.conf.d/52edgeguard-auto-updates.",
|
||||
"autoUpdateToggled": "Auto-update setting saved.",
|
||||
"autoUpdateFailed": "Auto-update toggle failed",
|
||||
"updateChannelCardTitle": "Update channel",
|
||||
"updateChannelStable": "Stable",
|
||||
"updateChannelTesting": "Testing",
|
||||
"updateChannelApply": "Apply",
|
||||
"updateChannelConfirmTitle": "Switch update channel?",
|
||||
"updateChannelSwitchTestingWarn": "Testing may contain unstable interim builds. Both nodes will be switched.",
|
||||
"updateChannelSwitchStableWarn": "Switching back to stable may trigger a downgrade on both nodes (testing versions are dated newer).",
|
||||
"updateChannelSaved": "Update channel saved (both nodes).",
|
||||
"updateChannelFailed": "Update channel switch failed",
|
||||
"updateChannelDrift": "Channel drift detected against the peer node (peer: {{peer}}) — will sync on the next switch.",
|
||||
"updateChannelHint": "Testing pulls date-stamped interim builds from the testing repo, stable pulls curated releases. The channel applies to both HA nodes in sync.",
|
||||
"ipv6CardTitle": "IPv6",
|
||||
"ipv6On": "Enabled — HAProxy binds on [::]:80, [::]:443 and [::]:3443 in addition to IPv4.",
|
||||
"ipv6Off": "Disabled — HAProxy listens on IPv4 only.",
|
||||
@@ -1503,6 +1520,12 @@
|
||||
"emptyEventsDesc": "Triggers (cert expiry, backup failure, cluster drift, license invalid, etc.) haven't fired any events yet. When they do, they land here and get delivered to the configured channels.",
|
||||
"noChannels": "no active channel",
|
||||
"confirmDelete": "Really delete channel {{name}}?",
|
||||
"acknowledge": "Acknowledge",
|
||||
"acknowledgeAll": "Acknowledge all",
|
||||
"acknowledged": "Acknowledged",
|
||||
"acked": "acknowledged",
|
||||
"open": "open",
|
||||
"confirmDeleteEvents": "Permanently delete {{n}} event(s)?",
|
||||
"kindWebhook": "Webhook (Slack/Discord/Teams/HTTP endpoint)",
|
||||
"kindEmail": "Email (SMTP)",
|
||||
"smtp": {
|
||||
@@ -1523,7 +1546,8 @@
|
||||
"time": "Time",
|
||||
"severity": "Severity",
|
||||
"subject": "Subject",
|
||||
"delivered": "Delivered"
|
||||
"delivered": "Delivered",
|
||||
"status": "Status"
|
||||
}
|
||||
},
|
||||
"remotes": {
|
||||
@@ -1869,7 +1893,11 @@
|
||||
"deleteFailed": "Delete failed",
|
||||
"secretSet": "Stored — leave empty to keep unchanged.",
|
||||
"secretUnset": "Nothing stored yet.",
|
||||
"tabs": { "settings": "Settings", "clients": "Clients (NAS)", "users": "Users" },
|
||||
"tabs": {
|
||||
"settings": "Settings",
|
||||
"clients": "Clients (NAS)",
|
||||
"users": "Users"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": "RADIUS active on this node",
|
||||
"listen": "Listen addresses"
|
||||
@@ -1923,6 +1951,9 @@
|
||||
"enabled": "Enabled",
|
||||
"mode": "Mode",
|
||||
"paranoia": "Paranoia Level",
|
||||
"crsPlugins": "App profiles (CRS plugins)",
|
||||
"crsPluginsHint": "Official OWASP CRS exclusion plugins for well-known apps — automatically disable the typical false-positive rules in a path-scoped way (e.g. Nextcloud WebDAV, WordPress editor). Cleaner than manual rule IDs.",
|
||||
"crsPluginsPlaceholder": "Select app profiles (optional)",
|
||||
"exclusions": "Rule Exclusions",
|
||||
"exclusionsHint": "Comma-separated rule IDs to disable (e.g. 920350, 941130).",
|
||||
"trustedProxies": "Trusted Proxies",
|
||||
@@ -1933,11 +1964,19 @@
|
||||
"saveFailed": "Failed to save WAF configuration.",
|
||||
"noExclusions": "No rule exclusions yet.",
|
||||
"noNote": "No note",
|
||||
"exclusionsAddHint": "Add exceptions via the Alerts tab — click \"Add exception\" on an alert."
|
||||
"exclusionsAddHint": "Search for a rule above and add it — or via the Alerts tab with \"Add exception\" on an alert.",
|
||||
"exclusionAddPlaceholder": "Search rule (ID or description)…",
|
||||
"exclusionAddNote": "Note (optional)",
|
||||
"exclusionAddNotFound": "No rule found",
|
||||
"exclusionAddBtn": "Add",
|
||||
"appProfiles": "Custom App Profiles",
|
||||
"appProfilesHint": "Reusable, self-maintained profiles (bundles of rule IDs) — create them centrally under “App Profiles” and assign them here per domain. Complements the OWASP plugins above.",
|
||||
"appProfilesPlaceholder": "Select custom profiles (optional)"
|
||||
},
|
||||
"tabs": {
|
||||
"domains": "Domains",
|
||||
"alerts": "Alerts"
|
||||
"alerts": "Alerts",
|
||||
"profiles": "App Profiles"
|
||||
},
|
||||
"alerts": {
|
||||
"total": "entries",
|
||||
@@ -1966,6 +2005,39 @@
|
||||
"exceptionModalHint": "Optional: describe why this rule is a false positive for this domain.",
|
||||
"exceptionNotePlaceholder": "e.g. Our custom API uses non-standard headers that trigger this rule.",
|
||||
"alreadyExcluded": "Already excluded"
|
||||
},
|
||||
"profiles": {
|
||||
"intro": "Reusable exclusion profiles (bundles of CRS rule IDs). Create once, assign per domain. The built-in OWASP plugins are read-only and selected per domain.",
|
||||
"new": "New Profile",
|
||||
"empty": "No custom profiles yet. Create one to reuse rule exclusions.",
|
||||
"builtinInfo": "Built-in OWASP CRS plugins (file-based, maintained). Read-only — selectable per domain in the WAF drawer. “Use as template” creates an empty custom profile from it to fill in.",
|
||||
"typeBuiltin": "OWASP",
|
||||
"typeCustom": "Custom",
|
||||
"col": {
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"description": "Description",
|
||||
"rules": "Exclusions"
|
||||
},
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"clone": "Clone",
|
||||
"asTemplate": "Use as template",
|
||||
"deleteConfirm": "Really delete this profile? Domains using it will lose these exclusions.",
|
||||
"createTitle": "New App Profile",
|
||||
"editTitle": "Edit App Profile",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "e.g. My WebApp",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "What is this profile for? (optional)",
|
||||
"rules": "Rule Exclusions",
|
||||
"rulesHint": "CRS rule IDs disabled for assigned domains. Searchable by ID or description.",
|
||||
"rulesPlaceholder": "Search and add rule IDs…",
|
||||
"saved": "Profile saved.",
|
||||
"saveFailed": "Could not save profile.",
|
||||
"deleted": "Profile deleted.",
|
||||
"deleteFailed": "Could not delete profile.",
|
||||
"cloneSuffix": "Copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
@@ -41,6 +42,7 @@ interface AlertEvent {
|
||||
message: string
|
||||
sent_to: SendResult[]
|
||||
fired_at: string
|
||||
acknowledged_at?: string | null
|
||||
}
|
||||
|
||||
interface ChannelFormValues {
|
||||
@@ -69,6 +71,14 @@ export default function AlertsPage() {
|
||||
const qc = useQueryClient()
|
||||
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||
|
||||
// Tab über ?tab= steuerbar, damit der Dashboard-Deeplink
|
||||
// (/alerts?tab=events) direkt auf der Event-History landet statt auf
|
||||
// dem Channels-Tab. Default bleibt 'channels' für den Direktaufruf.
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const activeTab = searchParams.get('tab') === 'events' ? 'events' : 'channels'
|
||||
const setActiveTab = (key: string) =>
|
||||
setSearchParams(key === 'events' ? { tab: 'events' } : {}, { replace: true })
|
||||
|
||||
const channels = useQuery({
|
||||
queryKey: ['alerts', 'channels'],
|
||||
queryFn: async () => {
|
||||
@@ -90,6 +100,29 @@ export default function AlertsPage() {
|
||||
const [form] = Form.useForm<ChannelFormValues>()
|
||||
const [filterSev, setFilterSev] = useState<string | undefined>()
|
||||
const [filterKind, setFilterKind] = useState<string | undefined>()
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([])
|
||||
|
||||
// Nach Quittieren/Löschen sowohl die Event-Liste als auch die Dashboard-
|
||||
// Karte (queryKey ['alerts','events','recent']) invalidieren — Prefix-Match.
|
||||
const refreshEvents = () => {
|
||||
setSelectedIds([])
|
||||
void qc.invalidateQueries({ queryKey: ['alerts', 'events'] })
|
||||
}
|
||||
const ackMut = useMutation({
|
||||
mutationFn: (ids: number[]) => apiClient.post('/alerts/events/acknowledge', { ids }),
|
||||
onSuccess: () => { message.success(t('alerts.acknowledged')); refreshEvents() },
|
||||
onError: (e: Error) => message.error(e.message),
|
||||
})
|
||||
const ackAllMut = useMutation({
|
||||
mutationFn: () => apiClient.post('/alerts/events/acknowledge-all'),
|
||||
onSuccess: () => { message.success(t('alerts.acknowledged')); refreshEvents() },
|
||||
onError: (e: Error) => message.error(e.message),
|
||||
})
|
||||
const delEventsMut = useMutation({
|
||||
mutationFn: (ids: number[]) => apiClient.post('/alerts/events/delete', { ids }),
|
||||
onSuccess: () => { message.success(t('common.delete')); refreshEvents() },
|
||||
onError: (e: Error) => message.error(e.message),
|
||||
})
|
||||
|
||||
const kindOptions = useMemo(() => {
|
||||
const kinds = [...new Set((events.data ?? []).map(e => e.kind))].sort()
|
||||
@@ -267,6 +300,14 @@ export default function AlertsPage() {
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('alerts.col.status'), key: 'ack', width: 110,
|
||||
render: (_, r: AlertEvent) => r.acknowledged_at
|
||||
? <Tooltip title={dayjs(r.acknowledged_at).format('YYYY-MM-DD HH:mm')}>
|
||||
<Tag color="green">{t('alerts.acked')}</Tag>
|
||||
</Tooltip>
|
||||
: <Tag color="gold">{t('alerts.open')}</Tag>,
|
||||
},
|
||||
]
|
||||
|
||||
const kind = Form.useWatch('kind', form)
|
||||
@@ -304,7 +345,7 @@ export default function AlertsPage() {
|
||||
description={t('alerts.scopeDesc')}
|
||||
/>
|
||||
|
||||
<Tabs items={[
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} items={[
|
||||
{
|
||||
key: 'channels',
|
||||
label: t('alerts.tabs.channels'),
|
||||
@@ -342,33 +383,62 @@ export default function AlertsPage() {
|
||||
label: t('alerts.tabs.events'),
|
||||
children: (
|
||||
<Card size="small">
|
||||
<Space className="mb-8">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder={t('alerts.col.severity')}
|
||||
style={{ width: 150 }}
|
||||
value={filterSev}
|
||||
onChange={setFilterSev}
|
||||
options={[
|
||||
{ value: 'info', label: <Tag color="blue">INFO</Tag> },
|
||||
{ value: 'warning', label: <Tag color="orange">WARNING</Tag> },
|
||||
{ value: 'error', label: <Tag color="red">ERROR</Tag> },
|
||||
{ value: 'critical', label: <Tag color="magenta">CRITICAL</Tag> },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
placeholder={t('alerts.col.kind')}
|
||||
style={{ width: 220 }}
|
||||
value={filterKind}
|
||||
onChange={setFilterKind}
|
||||
options={kindOptions}
|
||||
/>
|
||||
</Space>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }} className="mb-8">
|
||||
<Space>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder={t('alerts.col.severity')}
|
||||
style={{ width: 150 }}
|
||||
value={filterSev}
|
||||
onChange={setFilterSev}
|
||||
options={[
|
||||
{ value: 'info', label: <Tag color="blue">INFO</Tag> },
|
||||
{ value: 'warning', label: <Tag color="orange">WARNING</Tag> },
|
||||
{ value: 'error', label: <Tag color="red">ERROR</Tag> },
|
||||
{ value: 'critical', label: <Tag color="magenta">CRITICAL</Tag> },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
placeholder={t('alerts.col.kind')}
|
||||
style={{ width: 220 }}
|
||||
value={filterKind}
|
||||
onChange={setFilterKind}
|
||||
options={kindOptions}
|
||||
/>
|
||||
</Space>
|
||||
<Space>
|
||||
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||
<Button size="small" disabled={isViewer || selectedIds.length === 0}
|
||||
loading={ackMut.isPending} onClick={() => ackMut.mutate(selectedIds)}>
|
||||
{t('alerts.acknowledge')}{selectedIds.length > 0 ? ` (${selectedIds.length})` : ''}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popconfirm title={t('alerts.confirmDeleteEvents', { n: selectedIds.length })}
|
||||
onConfirm={() => delEventsMut.mutate(selectedIds)}
|
||||
disabled={isViewer || selectedIds.length === 0}>
|
||||
<Button size="small" danger disabled={isViewer || selectedIds.length === 0}
|
||||
loading={delEventsMut.isPending}>
|
||||
{t('common.delete')}{selectedIds.length > 0 ? ` (${selectedIds.length})` : ''}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||
<Button size="small" type="primary" ghost disabled={isViewer}
|
||||
loading={ackAllMut.isPending} onClick={() => ackAllMut.mutate()}>
|
||||
{t('alerts.acknowledgeAll')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
<Table size="small" rowKey="id" loading={events.isFetching}
|
||||
dataSource={filteredEvents} columns={evColumns}
|
||||
pagination={{ pageSize: 25 }}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedIds,
|
||||
onChange: (keys) => setSelectedIds(keys as number[]),
|
||||
}}
|
||||
rowClassName={(r) => (r.acknowledged_at ? 'eg-row-muted' : '')}
|
||||
pagination={{ defaultPageSize: 25 }}
|
||||
locale={{ emptyText: (
|
||||
<EmptyState
|
||||
icon={<BellOutlined />}
|
||||
|
||||
@@ -20,13 +20,17 @@ interface Backend {
|
||||
id: number; name: string; scheme: string
|
||||
health_check_path?: string | null
|
||||
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
|
||||
websocket: boolean; force_http1: boolean; active: boolean
|
||||
websocket: boolean; force_http1: boolean
|
||||
server_timeout_seconds?: number | null
|
||||
active: boolean
|
||||
}
|
||||
interface BackendFormValues {
|
||||
name: string; scheme: 'http' | 'https'
|
||||
health_check_path?: string
|
||||
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
|
||||
websocket: boolean; force_http1: boolean; active: boolean
|
||||
websocket: boolean; force_http1: boolean
|
||||
server_timeout_seconds?: number | null
|
||||
active: boolean
|
||||
domain_ids?: number[]
|
||||
}
|
||||
interface BackendServer {
|
||||
@@ -174,6 +178,7 @@ export default function BackendDetailPage() {
|
||||
lb_algorithm: backend.lb_algorithm,
|
||||
websocket: backend.websocket,
|
||||
force_http1: backend.force_http1,
|
||||
server_timeout_seconds: backend.server_timeout_seconds ?? undefined,
|
||||
active: backend.active,
|
||||
domain_ids: attached.map(d => d.id),
|
||||
}}
|
||||
@@ -205,6 +210,11 @@ export default function BackendDetailPage() {
|
||||
extra={t('backends.forceHttp1Hint')}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('backends.serverTimeout')} name="server_timeout_seconds"
|
||||
extra={t('backends.serverTimeoutHint')}>
|
||||
<InputNumber min={1} max={86400} step={30}
|
||||
style={{ width: '100%' }} addonAfter="s" placeholder="60 (default)" />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('backends.active')} name="active" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
@@ -26,6 +26,7 @@ interface Backend {
|
||||
lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
|
||||
websocket: boolean
|
||||
force_http1: boolean
|
||||
server_timeout_seconds?: number | null
|
||||
active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -266,6 +267,7 @@ export default function BackendsPage() {
|
||||
<Space size={4}>
|
||||
<Tag>{v}</Tag>
|
||||
{row.websocket && <Tag color="cyan">WS</Tag>}
|
||||
{row.server_timeout_seconds ? <Tag color="gold">{row.server_timeout_seconds}s</Tag> : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -350,7 +350,7 @@ export default function HistoryTab() {
|
||||
loading={list.isFetching}
|
||||
dataSource={list.data ?? []}
|
||||
columns={columns}
|
||||
pagination={{ pageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }}
|
||||
pagination={{ defaultPageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }}
|
||||
locale={{ emptyText: t('backups.empty') }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ function DecisionsTab() {
|
||||
loading={isLoading}
|
||||
dataSource={decisions ?? []}
|
||||
columns={columns}
|
||||
pagination={{ pageSize: 20 }}
|
||||
pagination={{ defaultPageSize: 20 }}
|
||||
/>
|
||||
<Modal
|
||||
title={t('cs.decision.banModal')}
|
||||
@@ -321,7 +321,7 @@ function AlertsTab() {
|
||||
loading={isLoading}
|
||||
dataSource={alerts ?? []}
|
||||
columns={columns}
|
||||
pagination={{ pageSize: 20 }}
|
||||
pagination={{ defaultPageSize: 20 }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -379,7 +379,7 @@ function BouncersTab() {
|
||||
loading={isLoading}
|
||||
dataSource={bouncers ?? []}
|
||||
columns={columns}
|
||||
pagination={{ pageSize: 20 }}
|
||||
pagination={{ defaultPageSize: 20 }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -436,7 +436,7 @@ function MachinesTab() {
|
||||
loading={isLoading}
|
||||
dataSource={machines ?? []}
|
||||
columns={columns}
|
||||
pagination={{ pageSize: 20 }}
|
||||
pagination={{ defaultPageSize: 20 }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -515,7 +515,7 @@ function CollectionsTab() {
|
||||
loading={isLoading}
|
||||
dataSource={collections ?? []}
|
||||
columns={columns}
|
||||
pagination={{ pageSize: 50 }}
|
||||
pagination={{ defaultPageSize: 50 }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -201,7 +201,7 @@ export default function DashboardPage() {
|
||||
})
|
||||
const recentAlerts = useQuery({
|
||||
queryKey: ['alerts', 'events', 'recent'],
|
||||
queryFn: () => fetchList<AlertEvent>('/alerts/events?limit=10', 'events'),
|
||||
queryFn: () => fetchList<AlertEvent>('/alerts/events?limit=10&open=true', 'events'),
|
||||
refetchInterval: 60_000,
|
||||
})
|
||||
const services = useQuery({
|
||||
@@ -303,6 +303,13 @@ export default function DashboardPage() {
|
||||
return down
|
||||
})()
|
||||
|
||||
// Auf dem keepalived-BACKUP-Node erreicht die lokale HAProxy die Backend-
|
||||
// Subnetze nicht (die VLAN-Gateway-VIPs liegen beim Master) → sie sieht
|
||||
// ALLE Backends als down. Das ist strukturell erwartet, kein Ausfall:
|
||||
// der Master bedient den Traffic. Deshalb den roten Down-Alarm auf dem
|
||||
// Standby durch einen ruhigen Hinweis ersetzen.
|
||||
const isBackup = vipStatus.data?.vrrp_state === 'BACKUP'
|
||||
|
||||
const alerts = recentAlerts.data ?? []
|
||||
const nCritical = alerts.filter(e => e.severity === 'critical' || e.severity === 'error').length
|
||||
const nWarning = alerts.filter(e => e.severity === 'warning').length
|
||||
@@ -361,18 +368,25 @@ export default function DashboardPage() {
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
action={<Link to="/alerts" style={{ fontSize: 12 }}>{t('dashboard.alertsCard.viewAll')} →</Link>}
|
||||
action={<Link to="/alerts?tab=events" style={{ fontSize: 12 }}>{t('dashboard.alertsCard.viewAll')} →</Link>}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ─ Operational alerts ─────────────────────────────── */}
|
||||
{downBackends.length > 0 && (
|
||||
{downBackends.length > 0 && !isBackup && (
|
||||
<Alert
|
||||
type="error" showIcon className="mb-12"
|
||||
message={t('dashboard.downBackendsAlert', { count: downBackends.length })}
|
||||
description={<Space wrap size={4}>{downBackends.map(n => <Link key={n} to="/backends">{n}</Link>)}</Space>}
|
||||
/>
|
||||
)}
|
||||
{downBackends.length > 0 && isBackup && (
|
||||
<Alert
|
||||
type="info" showIcon className="mb-12"
|
||||
message={t('dashboard.backupNodeBackends')}
|
||||
description={t('dashboard.backupNodeBackendsDesc')}
|
||||
/>
|
||||
)}
|
||||
{maintenanceDomains.length > 0 && (
|
||||
<Alert
|
||||
type="warning" showIcon className="mb-12"
|
||||
|
||||
@@ -30,6 +30,7 @@ interface Domain {
|
||||
redirect_to: string
|
||||
rate_limit_rps: number; max_body_kb: number
|
||||
disable_h3: boolean
|
||||
crowdsec_trusted: boolean
|
||||
notes?: string | null
|
||||
}
|
||||
|
||||
@@ -44,6 +45,7 @@ interface DomainFormValues {
|
||||
redirect_to: string
|
||||
rate_limit_rps: number; max_body_kb: number
|
||||
disable_h3: boolean
|
||||
crowdsec_trusted: boolean
|
||||
notes?: string
|
||||
}
|
||||
|
||||
@@ -278,6 +280,7 @@ export default function DomainDetailPage() {
|
||||
rate_limit_rps: domain.rate_limit_rps ?? 0,
|
||||
max_body_kb: domain.max_body_kb ?? 0,
|
||||
disable_h3: domain.disable_h3 ?? false,
|
||||
crowdsec_trusted: domain.crowdsec_trusted ?? false,
|
||||
notes: domain.notes ?? '',
|
||||
}}
|
||||
onFinish={(v) => update.mutate(v)}
|
||||
@@ -367,6 +370,11 @@ export default function DomainDetailPage() {
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('domains.crowdsecTrusted')} name="crowdsec_trusted" valuePropName="checked"
|
||||
extra={t('domains.crowdsecTrustedHint')}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('domains.notes')} name="notes">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -129,6 +129,12 @@ export default function FirewallLivePage() {
|
||||
pausedRef.current = paused
|
||||
const pendingDuringPauseRef = useRef<Entry[]>([])
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
// tRef statt `t` in den WS-Effect-Deps: `t` wechselt bei jedem
|
||||
// i18n-Store-Update die Identität. Stünde es in den Deps, würde der
|
||||
// Effect neu laufen — inkl. `setEntries([])` und WS-Reconnect, d. h.
|
||||
// der Live-Puffer wäre ohne erkennbaren Grund plötzlich leer.
|
||||
const tRef = useRef(t)
|
||||
useEffect(() => { tRef.current = t }, [t])
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setAppliedFilters(filters), 300)
|
||||
@@ -187,7 +193,7 @@ export default function FirewallLivePage() {
|
||||
if (!cancelled && active) scheduleReconnect()
|
||||
}
|
||||
ws.onerror = () => {
|
||||
setError(t('fwlog.connError'))
|
||||
setError(tRef.current('fwlog.connError'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +210,7 @@ export default function FirewallLivePage() {
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer)
|
||||
if (wsRef.current) { wsRef.current.close(); wsRef.current = null }
|
||||
}
|
||||
}, [active, query, t])
|
||||
}, [active, query])
|
||||
|
||||
// Resume: gebufferte Events in die Tabelle mergen.
|
||||
useEffect(() => {
|
||||
@@ -412,7 +418,7 @@ export default function FirewallLivePage() {
|
||||
size="small"
|
||||
dataSource={[...entries].reverse()}
|
||||
columns={columns}
|
||||
pagination={{ pageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }}
|
||||
pagination={{ defaultPageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }}
|
||||
locale={{ emptyText: connected ? t('fwlog.empty') : t('fwlog.connecting') }}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -293,7 +293,7 @@ export default function LogsPage() {
|
||||
loading={logsQuery.isFetching}
|
||||
dataSource={entries}
|
||||
columns={columns}
|
||||
pagination={{ pageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }}
|
||||
pagination={{ defaultPageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }}
|
||||
/>
|
||||
|
||||
{entries.length === 0 && !logsQuery.isFetching && (
|
||||
|
||||
@@ -228,7 +228,7 @@ export default function RoutesTab() {
|
||||
size="small"
|
||||
dataSource={live.data ?? []}
|
||||
columns={liveColumns}
|
||||
pagination={{ pageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }}
|
||||
pagination={{ defaultPageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }}
|
||||
locale={{ emptyText: t('routes.liveEmpty') }}
|
||||
style={{ marginTop: 12 }}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Alert, Button, Card, Descriptions, Form, Input, InputNumber, Popconfirm, Select, Space, Spin, Switch, Tooltip, Typography, message } from 'antd'
|
||||
import { ApartmentOutlined, CloudDownloadOutlined, CloudSyncOutlined, CodeOutlined, CopyOutlined, DatabaseOutlined, DownloadOutlined, ExclamationCircleOutlined, FileSearchOutlined, GlobalOutlined, LockOutlined, MailOutlined, ReloadOutlined, SettingOutlined, StopOutlined, ToolOutlined } from '@ant-design/icons'
|
||||
import { ApartmentOutlined, BranchesOutlined, CloudDownloadOutlined, CloudSyncOutlined, CodeOutlined, CopyOutlined, DatabaseOutlined, DownloadOutlined, ExclamationCircleOutlined, FileSearchOutlined, GlobalOutlined, LockOutlined, MailOutlined, ReloadOutlined, SettingOutlined, StopOutlined, ToolOutlined } from '@ant-design/icons'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -342,6 +342,30 @@ export default function SettingsPage() {
|
||||
},
|
||||
})
|
||||
|
||||
const [channelDraft, setChannelDraft] = useState<string | null>(null)
|
||||
const { data: updateChannel } = useQuery({
|
||||
queryKey: ['cluster', 'update-channel'],
|
||||
queryFn: async () => {
|
||||
const r = await apiClient.get('/cluster/update-channel')
|
||||
return isEnvelope(r.data)
|
||||
? (r.data.data as { channel: string; peer_channel?: string; peer_reached: boolean; peer_drifted: boolean })
|
||||
: { channel: 'stable', peer_reached: false, peer_drifted: false }
|
||||
},
|
||||
})
|
||||
const setUpdateChannel = useMutation({
|
||||
mutationFn: async (channel: string) => {
|
||||
const r = await apiClient.post('/cluster/update-channel', { channel })
|
||||
return r.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
msg.success(t('settings.updateChannelSaved'))
|
||||
void qc.invalidateQueries({ queryKey: ['cluster', 'update-channel'] })
|
||||
void qc.invalidateQueries({ queryKey: ['system', 'package-versions'] })
|
||||
},
|
||||
onError: (e: Error) => msg.error(t('settings.updateChannelFailed') + ': ' + e.message),
|
||||
onSettled: () => setChannelDraft(null),
|
||||
})
|
||||
|
||||
const { data: ipv6 } = useQuery({
|
||||
queryKey: ['system', 'ipv6'],
|
||||
queryFn: async () => {
|
||||
@@ -765,6 +789,55 @@ export default function SettingsPage() {
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={<><BranchesOutlined /> {t('settings.updateChannelCardTitle')}</>}
|
||||
className="mb-12"
|
||||
size="small"
|
||||
>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Select
|
||||
value={channelDraft ?? updateChannel?.channel ?? 'stable'}
|
||||
style={{ width: 160 }}
|
||||
disabled={isViewer}
|
||||
options={[
|
||||
{ value: 'stable', label: t('settings.updateChannelStable') },
|
||||
{ value: 'testing', label: t('settings.updateChannelTesting') },
|
||||
]}
|
||||
onChange={setChannelDraft}
|
||||
/>
|
||||
{channelDraft && channelDraft !== (updateChannel?.channel ?? 'stable') && (
|
||||
<Popconfirm
|
||||
title={t('settings.updateChannelConfirmTitle')}
|
||||
description={
|
||||
channelDraft === 'testing'
|
||||
? t('settings.updateChannelSwitchTestingWarn')
|
||||
: t('settings.updateChannelSwitchStableWarn')
|
||||
}
|
||||
okText={t('settings.updateChannelApply')}
|
||||
cancelText={t('common.cancel')}
|
||||
onConfirm={() => setUpdateChannel.mutate(channelDraft)}
|
||||
onCancel={() => setChannelDraft(null)}
|
||||
>
|
||||
<Button size="small" type="primary" loading={setUpdateChannel.isPending}>
|
||||
{t('settings.updateChannelApply')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
{updateChannel?.peer_drifted && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={t('settings.updateChannelDrift', { peer: updateChannel?.peer_channel ?? '?' })}
|
||||
/>
|
||||
)}
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('settings.updateChannelHint')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={<><GlobalOutlined /> {t('settings.ipv6CardTitle')}</>}
|
||||
className="mb-12"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Alert, Button, Card, Col, Drawer, Form, Input, Modal, Popconfirm, Row,
|
||||
Select, Space, Switch, Tabs, Tag, Tooltip, Typography, message,
|
||||
} from 'antd'
|
||||
import {
|
||||
CheckCircleOutlined, CloseCircleOutlined, DeleteOutlined,
|
||||
SafetyCertificateOutlined, SettingOutlined, WarningOutlined,
|
||||
CheckCircleOutlined, CloseCircleOutlined, CopyOutlined, DeleteOutlined,
|
||||
EditOutlined, PlusOutlined, SafetyCertificateOutlined, SettingOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
@@ -14,7 +15,7 @@ import apiClient, { isEnvelope } from '../../api/client'
|
||||
import PageHeader from '../../components/PageHeader'
|
||||
import DataTable from '../../components/DataTable'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { getRuleDescription } from './crsRules'
|
||||
import { CRS_RULES, getRuleDescription } from './crsRules'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
@@ -33,11 +34,30 @@ interface WafConfig {
|
||||
mode: 'detection' | 'blocking'
|
||||
paranoia_level: number
|
||||
rule_exclusions: string[]
|
||||
crs_plugins: string[]
|
||||
app_profiles: string[]
|
||||
exclusion_notes: Record<string, string>
|
||||
trusted_proxies: string[]
|
||||
custom_rules: string
|
||||
}
|
||||
|
||||
interface WafProfile {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
rule_exclusions: string[]
|
||||
builtin: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
// CRS-App-Exclusion-Plugins — muss zur Backend-Whitelist (KnownCRSPlugins) passen.
|
||||
const CRS_PLUGIN_OPTIONS = [
|
||||
{ value: 'nextcloud', label: 'Nextcloud' },
|
||||
{ value: 'wordpress', label: 'WordPress' },
|
||||
{ value: 'drupal', label: 'Drupal' },
|
||||
]
|
||||
|
||||
// ---------- API helpers -----------------------------------------------------
|
||||
|
||||
async function fetchDomains(): Promise<Domain[]> {
|
||||
@@ -65,16 +85,26 @@ function defaultConfig(domainId: number): WafConfig {
|
||||
mode: 'detection',
|
||||
paranoia_level: 1,
|
||||
rule_exclusions: [],
|
||||
crs_plugins: [],
|
||||
app_profiles: [],
|
||||
exclusion_notes: {},
|
||||
trusted_proxies: [],
|
||||
custom_rules: '',
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProfiles(): Promise<WafProfile[]> {
|
||||
const r = await apiClient.get('/waf/profiles')
|
||||
if (!isEnvelope(r.data)) return []
|
||||
return (r.data.data as { profiles?: WafProfile[] }).profiles ?? []
|
||||
}
|
||||
|
||||
interface WafFormValues {
|
||||
enabled: boolean
|
||||
mode: 'detection' | 'blocking'
|
||||
paranoia_level: number
|
||||
crs_plugins: string[]
|
||||
app_profiles: string[]
|
||||
trusted_proxies_str: string
|
||||
custom_rules: string
|
||||
}
|
||||
@@ -92,6 +122,14 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
const qc = useQueryClient()
|
||||
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||
const [form] = Form.useForm<WafFormValues>()
|
||||
const [addRuleId, setAddRuleId] = useState<string>('')
|
||||
const [addNote, setAddNote] = useState<string>('')
|
||||
// Durchsuchbare Optionen aus der CRS-Regel-Liste (ID — Beschreibung).
|
||||
// filterOption unten matcht sowohl ID als auch Beschreibung.
|
||||
const ruleOptions = useMemo(
|
||||
() => Object.entries(CRS_RULES).map(([id, desc]) => ({ value: id, label: `${id} — ${desc}` })),
|
||||
[],
|
||||
)
|
||||
|
||||
const { data: cfg, isLoading } = useQuery({
|
||||
queryKey: ['waf', 'config', domainId],
|
||||
@@ -99,6 +137,13 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
enabled: domainId !== null,
|
||||
})
|
||||
|
||||
// Eigene App-Profile (nur benutzerdefinierte, nicht built-in) als Optionen.
|
||||
const { data: profiles } = useQuery({ queryKey: ['waf', 'profiles'], queryFn: fetchProfiles })
|
||||
const profileOptions = useMemo(
|
||||
() => (profiles ?? []).filter(p => !p.builtin).map(p => ({ value: p.name, label: p.name })),
|
||||
[profiles],
|
||||
)
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: (values: WafConfig) =>
|
||||
apiClient.put(`/waf/configs/${domainId}`, values),
|
||||
@@ -136,6 +181,7 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
...cfg,
|
||||
app_profiles: cfg.app_profiles ?? [],
|
||||
trusted_proxies_str: (cfg.trusted_proxies ?? []).join(', '),
|
||||
}}
|
||||
onFinish={(vals) => {
|
||||
@@ -147,6 +193,8 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
mode: vals.mode,
|
||||
paranoia_level: vals.paranoia_level,
|
||||
rule_exclusions: cfg?.rule_exclusions ?? [],
|
||||
crs_plugins: vals.crs_plugins ?? [],
|
||||
app_profiles: vals.app_profiles ?? [],
|
||||
exclusion_notes: cfg?.exclusion_notes ?? {},
|
||||
trusted_proxies: proxies,
|
||||
custom_rules: vals.custom_rules ?? '',
|
||||
@@ -190,6 +238,35 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('waf.config.crsPlugins')}
|
||||
name="crs_plugins"
|
||||
help={t('waf.config.crsPluginsHint')}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
disabled={isViewer}
|
||||
placeholder={t('waf.config.crsPluginsPlaceholder')}
|
||||
options={CRS_PLUGIN_OPTIONS}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('waf.config.appProfiles')}
|
||||
name="app_profiles"
|
||||
help={t('waf.config.appProfilesHint')}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
disabled={isViewer}
|
||||
placeholder={t('waf.config.appProfilesPlaceholder')}
|
||||
options={profileOptions}
|
||||
notFoundContent={t('waf.profiles.empty')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* Exclusions list — shows existing exclusions with notes + remove button */}
|
||||
<Form.Item label={t('waf.config.exclusions')}>
|
||||
{(cfg?.rule_exclusions ?? []).length === 0 ? (
|
||||
@@ -224,6 +301,8 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
mode: cfg?.mode ?? 'detection',
|
||||
paranoia_level: cfg?.paranoia_level ?? 1,
|
||||
rule_exclusions: newExclusions,
|
||||
crs_plugins: cfg?.crs_plugins ?? [],
|
||||
app_profiles: cfg?.app_profiles ?? [],
|
||||
exclusion_notes: newNotes,
|
||||
trusted_proxies: cfg?.trusted_proxies ?? [],
|
||||
custom_rules: cfg?.custom_rules ?? '',
|
||||
@@ -235,9 +314,59 @@ function ConfigDrawer({ domainName, domainId, onClose }: ConfigDrawerProps) {
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>{t('waf.config.exclusionsAddHint')}</Text>
|
||||
</div>
|
||||
{!isViewer && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Select
|
||||
showSearch
|
||||
value={addRuleId || undefined}
|
||||
placeholder={t('waf.config.exclusionAddPlaceholder')}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
options={ruleOptions}
|
||||
optionFilterProp="label"
|
||||
notFoundContent={t('waf.config.exclusionAddNotFound')}
|
||||
onChange={(v) => setAddRuleId(v)}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t('waf.config.exclusionAddNote')}
|
||||
value={addNote}
|
||||
onChange={(e) => setAddNote(e.target.value)}
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
loading={save.isPending}
|
||||
disabled={!addRuleId}
|
||||
onClick={() => {
|
||||
if (!addRuleId) return
|
||||
const existing = cfg?.rule_exclusions ?? []
|
||||
if (existing.includes(addRuleId)) { setAddRuleId(''); setAddNote(''); return }
|
||||
const newNotes = { ...(cfg?.exclusion_notes ?? {}) }
|
||||
if (addNote.trim()) newNotes[addRuleId] = addNote.trim()
|
||||
save.mutate({
|
||||
domain_id: domainId!,
|
||||
enabled: cfg?.enabled ?? false,
|
||||
mode: cfg?.mode ?? 'detection',
|
||||
paranoia_level: cfg?.paranoia_level ?? 1,
|
||||
rule_exclusions: [...existing, addRuleId],
|
||||
crs_plugins: cfg?.crs_plugins ?? [],
|
||||
app_profiles: cfg?.app_profiles ?? [],
|
||||
exclusion_notes: newNotes,
|
||||
trusted_proxies: cfg?.trusted_proxies ?? [],
|
||||
custom_rules: cfg?.custom_rules ?? '',
|
||||
})
|
||||
setAddRuleId(''); setAddNote('')
|
||||
}}
|
||||
>
|
||||
{t('waf.config.exclusionAddBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 11, display: 'block', marginTop: 6 }}>
|
||||
{t('waf.config.exclusionsAddHint')}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
@@ -479,6 +608,220 @@ function AlertsTab({ domainId, configMap }: { domainId?: number; configMap: Map<
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- Profiles tab ----------------------------------------------------
|
||||
|
||||
interface ProfileFormValues {
|
||||
name: string
|
||||
description: string
|
||||
rule_exclusions: string[]
|
||||
}
|
||||
|
||||
function ProfileEditor({ profile, onClose }: { profile: WafProfile | null; onClose: () => void }) {
|
||||
const { t } = useTranslation()
|
||||
const qc = useQueryClient()
|
||||
const [form] = Form.useForm<ProfileFormValues>()
|
||||
const isCreate = profile !== null && profile.id === 0
|
||||
const ruleOptions = useMemo(
|
||||
() => Object.entries(CRS_RULES).map(([id, desc]) => ({ value: id, label: `${id} — ${desc}` })),
|
||||
[],
|
||||
)
|
||||
|
||||
// Formular bei jedem Öffnen/Wechsel neu befüllen.
|
||||
useEffect(() => {
|
||||
if (profile) {
|
||||
form.setFieldsValue({
|
||||
name: profile.name,
|
||||
description: profile.description,
|
||||
rule_exclusions: profile.rule_exclusions ?? [],
|
||||
})
|
||||
}
|
||||
}, [profile, form])
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: (vals: ProfileFormValues) =>
|
||||
isCreate
|
||||
? apiClient.post('/waf/profiles', vals)
|
||||
: apiClient.put(`/waf/profiles/${profile!.id}`, vals),
|
||||
onSuccess: () => {
|
||||
message.success(t('waf.profiles.saved'))
|
||||
void qc.invalidateQueries({ queryKey: ['waf'] })
|
||||
onClose()
|
||||
},
|
||||
onError: () => message.error(t('waf.profiles.saveFailed')),
|
||||
})
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={isCreate ? t('waf.profiles.createTitle') : t('waf.profiles.editTitle')}
|
||||
open={profile !== null}
|
||||
onClose={onClose}
|
||||
width={520}
|
||||
footer={
|
||||
<Space style={{ justifyContent: 'flex-end', width: '100%' }}>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button type="primary" loading={save.isPending} onClick={() => form.submit()}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={(vals) => save.mutate({
|
||||
name: vals.name,
|
||||
description: vals.description ?? '',
|
||||
rule_exclusions: vals.rule_exclusions ?? [],
|
||||
})}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('waf.profiles.name')}
|
||||
name="name"
|
||||
rules={[{ required: true, max: 60 }]}
|
||||
>
|
||||
<Input placeholder={t('waf.profiles.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('waf.profiles.description')} name="description">
|
||||
<Input placeholder={t('waf.profiles.descriptionPlaceholder')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('waf.profiles.rules')}
|
||||
name="rule_exclusions"
|
||||
help={t('waf.profiles.rulesHint')}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder={t('waf.profiles.rulesPlaceholder')}
|
||||
options={ruleOptions}
|
||||
optionFilterProp="label"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfilesTab() {
|
||||
const { t } = useTranslation()
|
||||
const qc = useQueryClient()
|
||||
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||
const [editing, setEditing] = useState<WafProfile | null>(null)
|
||||
|
||||
const { data: profiles, isLoading } = useQuery({ queryKey: ['waf', 'profiles'], queryFn: fetchProfiles })
|
||||
const custom = (profiles ?? []).filter(p => !p.builtin)
|
||||
|
||||
const del = useMutation({
|
||||
mutationFn: (id: number) => apiClient.delete(`/waf/profiles/${id}`),
|
||||
onSuccess: () => {
|
||||
message.success(t('waf.profiles.deleted'))
|
||||
void qc.invalidateQueries({ queryKey: ['waf'] })
|
||||
},
|
||||
onError: () => message.error(t('waf.profiles.deleteFailed')),
|
||||
})
|
||||
|
||||
// openCreate(seed) öffnet den Editor im Create-Modus (id=0) mit Startwerten.
|
||||
const openCreate = (seed?: Partial<WafProfile>) => setEditing({
|
||||
id: 0,
|
||||
name: seed?.name ?? '',
|
||||
description: seed?.description ?? '',
|
||||
rule_exclusions: seed?.rule_exclusions ?? [],
|
||||
builtin: false,
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('waf.profiles.col.name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (v: string) => <Text strong style={{ fontSize: 13 }}>{v}</Text>,
|
||||
},
|
||||
{
|
||||
title: t('waf.profiles.col.description'),
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
render: (v: string) => <Text type="secondary" style={{ fontSize: 12 }}>{v || '—'}</Text>,
|
||||
},
|
||||
{
|
||||
title: t('waf.profiles.col.rules'),
|
||||
key: 'rules',
|
||||
width: 90,
|
||||
render: (_: unknown, row: WafProfile) => <Tag>{(row.rule_exclusions ?? []).length}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
width: 210,
|
||||
render: (_: unknown, row: WafProfile) => (
|
||||
<Space size={4}>
|
||||
<Button size="small" icon={<EditOutlined />} disabled={isViewer} onClick={() => setEditing(row)}>
|
||||
{t('waf.profiles.edit')}
|
||||
</Button>
|
||||
<Tooltip title={t('waf.profiles.clone')}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
disabled={isViewer}
|
||||
onClick={() => openCreate({
|
||||
name: `${row.name} ${t('waf.profiles.cloneSuffix')}`,
|
||||
description: row.description,
|
||||
rule_exclusions: row.rule_exclusions,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm title={t('waf.profiles.deleteConfirm')} onConfirm={() => del.mutate(row.id)} disabled={isViewer}>
|
||||
<Button size="small" danger icon={<DeleteOutlined />} disabled={isViewer} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
className="mb-16"
|
||||
message={t('waf.profiles.intro')}
|
||||
description={
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text style={{ fontSize: 12 }}>{t('waf.profiles.builtinInfo')}</Text>
|
||||
<div style={{ marginTop: 8, display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
{CRS_PLUGIN_OPTIONS.map(p => (
|
||||
<Space key={p.value} size={2}>
|
||||
<Tag icon={<SafetyCertificateOutlined />}>{p.label}</Tag>
|
||||
{!isViewer && (
|
||||
<Button size="small" type="link" onClick={() => openCreate({ name: `${p.value}-custom` })}>
|
||||
{t('waf.profiles.asTemplate')}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="flex-between mb-12">
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{custom.length} {t('waf.profiles.typeCustom')}
|
||||
</Text>
|
||||
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={() => openCreate()}>
|
||||
{t('waf.profiles.new')}
|
||||
</Button>
|
||||
</div>
|
||||
{custom.length === 0 && !isLoading ? (
|
||||
<Alert type="info" showIcon message={t('waf.profiles.empty')} />
|
||||
) : (
|
||||
<DataTable rowKey="id" loading={isLoading} dataSource={custom} columns={columns} />
|
||||
)}
|
||||
<ProfileEditor profile={editing} onClose={() => setEditing(null)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- Page ------------------------------------------------------------
|
||||
|
||||
export default function WAFPage() {
|
||||
@@ -652,6 +995,11 @@ export default function WAFPage() {
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'profiles',
|
||||
label: t('waf.tabs.profiles'),
|
||||
children: <ProfilesTab />,
|
||||
},
|
||||
{
|
||||
key: 'alerts',
|
||||
label: t('waf.tabs.alerts'),
|
||||
|
||||
@@ -1160,6 +1160,9 @@ h1, h2, h3, h4, h5, h6 {
|
||||
.threat-tile.threat-tile--warn { opacity: 1; border-left: 3px solid #F59E0B; }
|
||||
.threat-tile.threat-tile--crit { opacity: 1; border-left: 3px solid #DC2626; }
|
||||
.threat-tile.threat-tile--idle { opacity: 0.55; border-left: 3px solid transparent; }
|
||||
|
||||
/* Quittierte Alert-Events (Alerts → Events-Tab) werden abgedimmt. */
|
||||
tr.eg-row-muted > td { opacity: 0.5; }
|
||||
.threat-tile-value--hit { color: #EF4444; }
|
||||
.threat-tile-value--warn { color: #F59E0B; }
|
||||
.threat-tile-value--crit { color: #DC2626; }
|
||||
|
||||
@@ -152,6 +152,9 @@ edgeguard ALL=(root) NOPASSWD: /usr/bin/apt-get update
|
||||
# NICHT beliebig in /etc/apt/apt.conf.d schreiben darf.
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/tee /etc/apt/apt.conf.d/52edgeguard-auto-updates
|
||||
edgeguard ALL=(root) NOPASSWD: /bin/rm -f /etc/apt/apt.conf.d/52edgeguard-auto-updates
|
||||
# Update-Kanal-Switch (Settings → Update-Kanal) schreibt exakt diese
|
||||
# sources.list-Zeile. Gleiches Restrict-Pattern wie oben.
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/tee /etc/apt/sources.list.d/edgeguard.list
|
||||
# Backup-Pfad: pg_dump als postgres-User. Whitelist exakt mit
|
||||
# --clean --if-exists --no-owner --no-acl + dem festen DB-Namen.
|
||||
edgeguard ALL=(postgres) NOPASSWD: /usr/bin/pg_dump --clean --if-exists --no-owner --no-acl edgeguard
|
||||
@@ -203,7 +206,9 @@ edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl stop edgeguard-waf.service
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl enable edgeguard-waf.service
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl disable edgeguard-waf.service
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl restart edgeguard-waf.service
|
||||
# CrowdSec service toggle (start/stop/enable/disable)
|
||||
# CrowdSec service toggle (start/stop/enable/disable) + reload (Whitelist-Render)
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reload crowdsec.service
|
||||
edgeguard ALL=(root) NOPASSWD: /bin/systemctl reload crowdsec.service
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl start crowdsec.service
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl stop crowdsec.service
|
||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl enable crowdsec.service
|
||||
@@ -292,6 +297,28 @@ SUDOERS
|
||||
|
||||
chmod 0440 /etc/sudoers.d/edgeguard
|
||||
|
||||
# ── Migration: alte "main"-Komponente → "stable" (Kanal-Modell) ──
|
||||
# Vor diesem Release schrieb der Installer immer Komponente "main"
|
||||
# in die apt-Quelle. Ab jetzt publiziert publish.sh/release.sh nur
|
||||
# noch nach stable/testing — ohne diese Migration würden
|
||||
# Bestandsnodes stumm keine neuen Updates mehr sehen (main bleibt
|
||||
# auf dem letzten main-Stand stehen). Idempotent.
|
||||
#
|
||||
# netcell-edgeguard.list: NOCH ältere Installer-Generation vor dem
|
||||
# Rename auf edgeguard.list (install.sh räumt sie nur bei einem
|
||||
# FRISCHEN Install auf, nie bei einem Upgrade — auf Bestandsnodes,
|
||||
# die nie neu installiert wurden, ist sie ggf. noch die aktive
|
||||
# Datei). Erst konsolidieren, dann main→stable.
|
||||
EG_SOURCES_LIST=/etc/apt/sources.list.d/edgeguard.list
|
||||
EG_LEGACY_LIST=/etc/apt/sources.list.d/netcell-edgeguard.list
|
||||
if [ -f "$EG_LEGACY_LIST" ] && [ ! -f "$EG_SOURCES_LIST" ]; then
|
||||
mv "$EG_LEGACY_LIST" "$EG_SOURCES_LIST"
|
||||
fi
|
||||
if [ -f "$EG_SOURCES_LIST" ] && grep -q ' trixie main$' "$EG_SOURCES_LIST"; then
|
||||
sed -i 's/ trixie main$/ trixie stable/' "$EG_SOURCES_LIST"
|
||||
apt-get update -qq || true
|
||||
fi
|
||||
|
||||
# ── Sysctl-Profil für Edge-Gateway (NAT + HAProxy + Forwarding) ──
|
||||
# Voraussetzung für NAT/DNAT/Masquerade + sinnvolle Defaults
|
||||
# für eine high-throughput Forwarding-Box. Edit nicht von Hand
|
||||
@@ -355,7 +382,14 @@ net.core.default_qdisc = fq
|
||||
net.ipv4.tcp_syncookies = 1
|
||||
net.ipv4.icmp_echo_ignore_broadcasts = 1
|
||||
net.ipv4.icmp_ignore_bogus_error_responses = 1
|
||||
net.ipv4.conf.all.log_martians = 1
|
||||
# log_martians AUS (nicht der rp_filter-Drop selbst!): Ein VRRP-Backup-Node
|
||||
# sieht auf geteilten L2-VLANs dauerhaft Broadcast/Multicast für Gateway-/
|
||||
# VIP-Adressen, die er im Backup-Zustand nicht besitzt — rp_filter=2 verwirft
|
||||
# das korrekt, aber log_martians=1 flutet dmesg/journal damit (>100k Zeilen/
|
||||
# Woche beobachtet). Kernel-OR-Semantik: conf.all=1 überschreibt jeden
|
||||
# Interface-spezifischen Wert, daher nur hier zentral abschaltbar. Security-
|
||||
# Sichtbarkeit läuft ohnehin über nftables-NFLOG/ulogd2 + CrowdSec, nicht dmesg.
|
||||
net.ipv4.conf.all.log_martians = 0
|
||||
kernel.kptr_restrict = 2
|
||||
kernel.dmesg_restrict = 1
|
||||
|
||||
@@ -376,9 +410,25 @@ SYSCTL
|
||||
if [ ! -f /var/log/edgeguard/firewall.jsonl ]; then
|
||||
: > /var/log/edgeguard/firewall.jsonl
|
||||
fi
|
||||
# ulogd2 läuft als root (eigener Daemon); File muss von ihm
|
||||
# schreibbar UND von edgeguard-API lesbar sein.
|
||||
chown root:"$EG_USER" /var/log/edgeguard/firewall.jsonl
|
||||
# WICHTIG (Incident 2026-09-06): ulogd2 läuft NICHT als root —
|
||||
# die Debian-Unit startet `ulogd --daemon --uid ulog`, der Daemon
|
||||
# dropped also nach dem Öffnen seiner Files auf den User `ulog`.
|
||||
# Solange er läuft schreibt er über den offenen fd weiter, egal
|
||||
# wem die Datei gehört. Aber: das Distro-Profil
|
||||
# /etc/logrotate.d/ulogd2 schickt nachts SIGHUP an ulogd, damit
|
||||
# es seine Logfiles neu öffnet — und DAS passiert als `ulog`.
|
||||
# Gehörte die Datei root:edgeguard 0640, scheitert das Reopen mit
|
||||
# "can't open JSON log file: Permission denied", ulogd wertet das
|
||||
# als fatal und BEENDET sich. Ohne Restart= blieb der Dienst dann
|
||||
# tagelang still tot (5 Tage unbemerkt) → Firewall-Live-Log zeigte
|
||||
# nur noch den alten In-Memory-Ring der API, nie neue Events.
|
||||
# Daher: Owner = ulog (Schreiber), Gruppe = edgeguard (Leser).
|
||||
if getent passwd ulog >/dev/null 2>&1; then
|
||||
chown ulog:"$EG_USER" /var/log/edgeguard/firewall.jsonl
|
||||
else
|
||||
# ulogd2 (noch) nicht installiert — Fallback wie bisher.
|
||||
chown root:"$EG_USER" /var/log/edgeguard/firewall.jsonl
|
||||
fi
|
||||
chmod 0640 /var/log/edgeguard/firewall.jsonl
|
||||
|
||||
cat > /etc/ulogd.conf <<'ULOGD'
|
||||
@@ -426,7 +476,9 @@ ULOGD
|
||||
compress
|
||||
delaycompress
|
||||
copytruncate
|
||||
create 0640 root edgeguard
|
||||
# Owner muss ulog sein — siehe Kommentar oben: ulogd re-öffnet die
|
||||
# Datei bei SIGHUP als `ulog` und stirbt sonst an EACCES.
|
||||
create 0640 ulog edgeguard
|
||||
}
|
||||
LOGROTATE
|
||||
chmod 0644 /etc/logrotate.d/edgeguard-firewall
|
||||
@@ -435,6 +487,18 @@ LOGROTATE
|
||||
# da ist (Dependency-Konflikt o.ä.), nur warnen — die Firewall
|
||||
# läuft auch ohne Logger.
|
||||
if systemctl list-unit-files ulogd2.service >/dev/null 2>&1; then
|
||||
# Restart=always als Selbstheilung: ulogd beendet sich bei
|
||||
# einem fehlgeschlagenen Reopen mit Exit-Code 0 (also KEIN
|
||||
# Restart=on-failure — das würde nicht greifen). Ohne das
|
||||
# bleibt Firewall-Logging nach einem Rotate-Hiccup still tot.
|
||||
install -d -m 0755 /etc/systemd/system/ulogd2.service.d
|
||||
cat > /etc/systemd/system/ulogd2.service.d/edgeguard-restart.conf <<'ULOGDUNIT'
|
||||
# Managed by edgeguard — re-installation overwrites this file.
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
ULOGDUNIT
|
||||
systemctl daemon-reload
|
||||
systemctl enable ulogd2.service >/dev/null 2>&1 || true
|
||||
systemctl restart ulogd2.service || \
|
||||
echo "postinst: ulogd2.service restart failed (firewall logs disabled until fixed)" >&2
|
||||
@@ -780,24 +844,20 @@ VIPCMD
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ALTER PUBLICATION erfordert den PG-Superuser (edgeguard ist nicht
|
||||
# Owner der Publication). Idempotent — No-Op auf Secondary-Nodes
|
||||
# (wo die Publication nicht existiert) und wenn die Tabellen schon
|
||||
# drin sind.
|
||||
sudo -u postgres psql edgeguard <<'EOSQL' 2>/dev/null || true
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'edgeguard_shared') THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_publication_tables
|
||||
WHERE pubname = 'edgeguard_shared' AND tablename = 'network_interfaces'
|
||||
) THEN
|
||||
ALTER PUBLICATION edgeguard_shared ADD TABLE network_interfaces, ip_addresses;
|
||||
END IF;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
EOSQL
|
||||
# ── Replikation reconcilen (Publication / Grants / Subscription) ──
|
||||
# Ersetzt die frühere feste ADD-TABLE-Logik (die network_interfaces/
|
||||
# ip_addresses bei JEDEM Upgrade fälschlich wieder in die Publication
|
||||
# zog — beide sind node-lokal). edgeguard-ctl erkennt die Rolle selbst
|
||||
# und bringt idempotent den Soll-Zustand:
|
||||
# Publisher : fehlende Shared-Tables ADD, node-lokale (localOnlyTables)
|
||||
# DROP, GRANT SELECT für den Replikations-User (deckt per
|
||||
# Migration neu hinzugekommene Tabellen ab — sonst hängt
|
||||
# deren tablesync in 'd').
|
||||
# Subscriber: neue Tabellen leeren + REFRESH PUBLICATION.
|
||||
# Standalone: No-Op.
|
||||
# Läuft als root → buildPsqlCmd nutzt `sudo -u postgres psql` (Superuser).
|
||||
# Best-effort: ein Reconcile-Fehler darf das Upgrade nie abbrechen.
|
||||
/usr/bin/edgeguard-ctl cluster-reconcile-replication 2>&1 || true
|
||||
|
||||
# ── CrowdSec IDS installation ─────────────────────────────────────────
|
||||
# Install CrowdSec if not present. We use the official CrowdSec APT repo.
|
||||
@@ -823,6 +883,83 @@ EOSQL
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── CrowdSec HAProxy-Acquisition auf journald zeigen ──────────
|
||||
# EdgeGuards HAProxy loggt nach journald (log /dev/log local0/1) —
|
||||
# es gibt KEINE /var/log/haproxy.log. `cscli setup` rät aber auf eine
|
||||
# datei-basierte Quelle → CrowdSec liest nichts → die HTTP-/CVE-
|
||||
# Szenarien laufen leer (WAF-artiger Web-Schutz tot). Wir setzen die
|
||||
# Acquisition deterministisch auf die haproxy.service-Journal-Unit
|
||||
# (analog zur sshd-Quelle). Läuft auf JEDEM configure (self-healing
|
||||
# auch für Bestandsinstalls). Admin-Custom bleibt unangetastet: nur
|
||||
# überschreiben, wenn Datei fehlt / noch die kaputte Datei-Default
|
||||
# (/var/log/haproxy.log) enthält / von uns stammt.
|
||||
if command -v cscli >/dev/null 2>&1; then
|
||||
CS_HAPROXY_ACQUIS="/etc/crowdsec/acquis.d/setup.haproxy.yaml"
|
||||
if [ ! -f "$CS_HAPROXY_ACQUIS" ] \
|
||||
|| grep -q '/var/log/haproxy.log' "$CS_HAPROXY_ACQUIS" 2>/dev/null \
|
||||
|| grep -q 'Managed by EdgeGuard' "$CS_HAPROXY_ACQUIS" 2>/dev/null; then
|
||||
install -d -m 0755 /etc/crowdsec/acquis.d
|
||||
cat > "$CS_HAPROXY_ACQUIS" <<'ACQUIS'
|
||||
# Managed by EdgeGuard. HAProxy loggt nach journald (log /dev/log local0/1),
|
||||
# es gibt keine /var/log/haproxy.log. journalctl-Quelle analog zur sshd-Acquis.
|
||||
source: journalctl
|
||||
journalctl_filter:
|
||||
- "_SYSTEMD_UNIT=haproxy.service"
|
||||
labels:
|
||||
type: haproxy
|
||||
ACQUIS
|
||||
# SIGHUP-Reload lädt die Acquisition neu; Restart als Fallback.
|
||||
systemctl reload crowdsec 2>/dev/null \
|
||||
|| systemctl restart crowdsec 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# CrowdSec-Whitelist: eigenen Management-Traffic (Backend
|
||||
# api_backend = ausschließlich edgeguard-api: Admin-UI + REST-API +
|
||||
# ACME) von ALLEN Scenarios ausnehmen. Die Admin-SPA feuert beim
|
||||
# Laden viele /api/-Requests — normal, wurde aber als http-crawl
|
||||
# gebannt (Incident 2026-08-03, Admin-IP gesperrt). IP-unabhängig.
|
||||
install -d -m 0755 /etc/crowdsec/parsers/s02-enrich
|
||||
# Altlast aus dem manuellen Live-Fix (2026-08-03) entfernen — sonst
|
||||
# lägen zwei inhaltsgleiche Whitelist-Dateien nebeneinander.
|
||||
rm -f /etc/crowdsec/parsers/s02-enrich/netcell-mgmt-whitelist.yaml
|
||||
cat > /etc/crowdsec/parsers/s02-enrich/edgeguard-mgmt-whitelist.yaml <<'WL'
|
||||
name: edgeguard/mgmt-ui-whitelist
|
||||
description: Management-UI/REST-API/ACME (HAProxy-Backend api_backend) von CrowdSec ausnehmen - Admin-SPA-Traffic ist kein Angriff.
|
||||
whitelist:
|
||||
reason: edgeguard management-ui / api (backend api_backend)
|
||||
expression:
|
||||
- "evt.Parsed.backend_name == 'api_backend'"
|
||||
WL
|
||||
systemctl reload crowdsec 2>/dev/null \
|
||||
|| systemctl restart crowdsec 2>/dev/null || true
|
||||
|
||||
# Admin-Hosts-Whitelist: wird vom crowdsec-whitelist-Generator aus
|
||||
# domains.crowdsec_trusted gerendert (UI-Schalter "vertrauenswuerdiges
|
||||
# Admin-Panel"). Das Verzeichnis ist root-owned → der Generator (laeuft
|
||||
# als edgeguard) kann nur eine vorab-chownte Datei ueberschreiben, daher
|
||||
# hier leer + edgeguard-owned anlegen, dann aus der DB befuellen.
|
||||
install -o "$EG_USER" -g "$EG_USER" -m 0644 /dev/null \
|
||||
/etc/crowdsec/parsers/s02-enrich/edgeguard-admin-hosts-whitelist.yaml
|
||||
sudo -n -u "$EG_USER" /usr/bin/edgeguard-ctl render-config --only=crowdsec-whitelist 2>/dev/null || true
|
||||
|
||||
# CrowdSec-Simulations-Default: http-crawl-non_statics ist bei modernen
|
||||
# SPAs/Apps strukturell FP-anfaellig — ein Seiten-Load/Sync feuert 40+
|
||||
# distinkte /api/-URLs, der Leaky-Bucket (capacity=40, leak ~2/s) laeuft
|
||||
# in Sekunden ueber → False-Positive-Ban legitimer Nutzer/Kunden. Daher
|
||||
# per Default in SIMULATION: alarmiert weiter, bannt aber nicht. Echte
|
||||
# Angriffe (ssh-bf, http-cve-*, backdoors, CVE-2021-41773 …) bleiben
|
||||
# scharf. Nur bei ERST-Install setzen (Marker) → ein spaeteres manuelles
|
||||
# 'cscli simulation disable …' des Operators wird bei Updates NICHT
|
||||
# wieder ueberschrieben.
|
||||
EG_CROWDSEC_SIM_MARKER=/var/lib/edgeguard/.crowdsec-crawl-sim-applied
|
||||
if [ ! -f "$EG_CROWDSEC_SIM_MARKER" ]; then
|
||||
cscli simulation enable crowdsecurity/http-crawl-non_statics 2>/dev/null || true
|
||||
install -d -m 0755 /var/lib/edgeguard
|
||||
: > "$EG_CROWDSEC_SIM_MARKER"
|
||||
systemctl reload crowdsec 2>/dev/null || systemctl restart crowdsec 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Render initial service configs ───────────────────────────
|
||||
# Writes /etc/edgeguard/haproxy/haproxy.cfg + nftables.d/
|
||||
# ruleset.nft from the (just-migrated, empty) PG state.
|
||||
@@ -910,6 +1047,43 @@ KEEPALIVEDDROPIN
|
||||
rm -rf "${CRS_TMP}"
|
||||
fi
|
||||
|
||||
# ── CRS App-Exclusion-Plugins (Nextcloud / WordPress / Drupal) ──────
|
||||
# Offizielle OWASP-CRS-Plugins (separate Repos coreruleset/<name>-plugin).
|
||||
# Ihre plugins/*.conf landen in <crs>/plugins/; der WAF-Renderer bindet
|
||||
# je Domain die GEWÄHLTEN ein (waf_configs.crs_plugins). Läuft auf JEDEM
|
||||
# configure (self-healing für Bestandsinstalls), aber nur wenn das
|
||||
# jeweilige Plugin noch fehlt — admin-Anpassungen bleiben unangetastet.
|
||||
if [ -d "$WAF_CRS_DIR/rules" ]; then
|
||||
install -d -m 0755 "$WAF_CRS_DIR/plugins"
|
||||
for plugin in nextcloud-rule-exclusions wordpress-rule-exclusions drupal-rule-exclusions; do
|
||||
if [ -f "$WAF_CRS_DIR/plugins/${plugin}-before.conf" ] \
|
||||
|| [ -f "$WAF_CRS_DIR/plugins/${plugin}-config.conf" ]; then
|
||||
continue
|
||||
fi
|
||||
P_TMP="$(mktemp -d)"
|
||||
P_OK=""
|
||||
# Plugin-Repos nutzen teils 'main', teils 'master' als Default-
|
||||
# Branch. curl -f (statt still einen 404 als Erfolg zu werten).
|
||||
for P_BR in main master; do
|
||||
if curl -fsSL --max-time 45 \
|
||||
"https://github.com/coreruleset/${plugin}-plugin/archive/refs/heads/${P_BR}.tar.gz" \
|
||||
-o "${P_TMP}/p.tgz" 2>/dev/null \
|
||||
&& tar xzf "${P_TMP}/p.tgz" -C "${P_TMP}" 2>/dev/null; then
|
||||
P_OK=1; break
|
||||
fi
|
||||
done
|
||||
if [ -n "$P_OK" ] && ls "${P_TMP}"/*/plugins/${plugin}-*.conf >/dev/null 2>&1; then
|
||||
install -m 0644 "${P_TMP}"/*/plugins/${plugin}-*.conf \
|
||||
"$WAF_CRS_DIR/plugins/" 2>/dev/null \
|
||||
&& echo "postinst: CRS-Plugin ${plugin} installiert"
|
||||
else
|
||||
echo "postinst: CRS-Plugin ${plugin} nicht installiert (Download/Layout)" >&2
|
||||
fi
|
||||
rm -rf "${P_TMP}"
|
||||
done
|
||||
chown -R "$EG_USER":"$EG_USER" "$WAF_CRS_DIR/plugins" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ── systemd: pick up new units + restart haproxy with our cfg
|
||||
systemctl daemon-reload
|
||||
systemctl restart haproxy.service || true
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
#!/bin/bash
|
||||
# Keepalived health check: edgeguard-api erreichbar?
|
||||
# Weight -50 → BACKUP gewinnt wenn Primary-API nicht antwortet.
|
||||
# Keepalived-Health-Check (chk_edgeguard).
|
||||
#
|
||||
# WICHTIG (Incident 2026-08-03): Dieser Check entscheidet mit, ob ein Node die
|
||||
# VIP übernehmen bzw. per Preempt zurückholen darf. Er darf NUR dann "gesund"
|
||||
# (exit 0) melden, wenn der Node Traffic WIRKLICH bedienen kann — nicht nur
|
||||
# "edgeguard-api-Prozess up". Sonst reißt ein frisch gebooteter / mitten im
|
||||
# Deploy befindlicher Node (haproxy noch nicht bereit) die VIP an sich, obwohl
|
||||
# er nichts bedient → Cluster-Ausfall, bis der Node hart abgeschaltet wird.
|
||||
#
|
||||
# ALLE Bedingungen müssen erfüllt sein, sonst exit 1 → Instanz geht in FAULT →
|
||||
# kein (Re-)Preempt, der gesunde Peer behält/bekommt die VIP.
|
||||
|
||||
# 1) edgeguard-api antwortet auf den Health-Endpoint (Unix-Socket oder Loopback)?
|
||||
curl -sf --max-time 2 --unix-socket /run/edgeguard/api.sock \
|
||||
http://localhost/api/v1/system/health > /dev/null 2>&1 \
|
||||
|| curl -sf --max-time 2 http://127.0.0.1:9443/api/v1/system/health > /dev/null 2>&1
|
||||
|| curl -sf --max-time 2 http://127.0.0.1:9443/api/v1/system/health > /dev/null 2>&1 \
|
||||
|| exit 1
|
||||
|
||||
# 2) haproxy-Prozess aktiv? Während Boot/Restart kurz false → Node bleibt in dem
|
||||
# Fenster Backup (kann nicht preempten).
|
||||
systemctl is-active --quiet haproxy 2>/dev/null || exit 1
|
||||
|
||||
# 3) haproxy hört wirklich auf dem oeffentlichen TLS-Port :443 (bindet = bedient)?
|
||||
# Faengt "Prozess up, aber Config kaputt / Port nicht gebunden" ab. :443 ist
|
||||
# architektur-bedingt immer gebunden (Public-TLS + ACME-Webroot + Mgmt-FQDN-
|
||||
# Fallback), also ein verlaessliches Ready-Signal.
|
||||
ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq ':443$' || exit 1
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -4,8 +4,20 @@
|
||||
# KEIN Auto-Promote — Split-Brain-Schutz durch manuelle Promotion.
|
||||
# Admin muss "edgeguard-ctl promote" ausführen wenn PG-Failover gewünscht.
|
||||
|
||||
logger -t keepalived -p daemon.warning \
|
||||
"MASTER: VIP übernommen — PG-Rolle ist noch '$(cat /var/lib/edgeguard/pg_role 2>/dev/null || echo standby)'. Für PG-Failover: edgeguard-ctl promote"
|
||||
# PG-Rolle ZUVERLÄSSIG über die Publication ermitteln — nicht über die evtl.
|
||||
# veraltete/fehlende Datei /var/lib/edgeguard/pg_role (die schrieb nur `promote`;
|
||||
# ein via cluster-init-replication eingerichteter Primary hat sie nie → Log sagte
|
||||
# fälschlich "standby"). Nur der Primary trägt die Publication edgeguard_shared
|
||||
# (Konvention, vgl. cluster_repair.go). Best-effort: bei psql-Fehler neutral.
|
||||
if sudo -u postgres psql -d edgeguard -tAc \
|
||||
"SELECT EXISTS(SELECT 1 FROM pg_publication WHERE pubname='edgeguard_shared')" \
|
||||
2>/dev/null | grep -q '^t$'; then
|
||||
logger -t keepalived -p daemon.warning \
|
||||
"MASTER: VIP übernommen — dieser Node ist bereits PG-Primary (kein promote nötig)."
|
||||
else
|
||||
logger -t keepalived -p daemon.warning \
|
||||
"MASTER: VIP übernommen — PG-Rolle ist 'standby'. Für PG-Failover: edgeguard-ctl promote"
|
||||
fi
|
||||
|
||||
# ── Upstream-ARP/Routing für die Failover-VIP(s) aktualisieren ──
|
||||
# Manche Hoster lernen die neue MAC einer Failover-IP NICHT zuverlässig über
|
||||
|
||||
@@ -1,27 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Cleanup-old: löscht alle EdgeGuard-Versionen im Gitea Package
|
||||
# Registry außer den letzten N (default 10).
|
||||
# Registry außer den letzten N (default 10) — pro Kanal (Komponente).
|
||||
#
|
||||
# Wird vom Makefile direkt nach erfolgreichem Upload aufgerufen. Reihen-
|
||||
# folge ist wichtig: ERST der neue Build hochladen, DANN die ältesten
|
||||
# wegschmeißen — sonst riskieren wir bei Cleanup-vor-Upload eine Lücke.
|
||||
# Wird vom Makefile/release.sh direkt nach erfolgreichem Upload
|
||||
# aufgerufen. Reihenfolge ist wichtig: ERST der neue Build hochladen,
|
||||
# DANN die ältesten wegschmeißen — sonst riskieren wir bei Cleanup-vor-
|
||||
# Upload eine Lücke.
|
||||
#
|
||||
# Stable-Schutz: Versionen, die als Git-Tag `v<version>` im Repo stehen,
|
||||
# werden im stable-Kanal NIE gelöscht (analog enconf STABLE_PROTECT) —
|
||||
# ein Kunde, der genau diese Version installiert hat, muss sie über
|
||||
# apt-get weiterhin ziehen können.
|
||||
#
|
||||
# Voraussetzungen:
|
||||
# - ~/.gitea-token mit write-Package-Scope
|
||||
# - jq + curl
|
||||
#
|
||||
# Aufruf:
|
||||
# ./cleanup-old.sh # KEEP=10 (default)
|
||||
# KEEP=20 ./cleanup-old.sh # mehr behalten
|
||||
# ./cleanup-old.sh # Kanal stable, KEEP=10 (default)
|
||||
# ./cleanup-old.sh testing # Kanal testing
|
||||
# KEEP=20 ./cleanup-old.sh stable # mehr behalten
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
KEEP="${KEEP:-10}"
|
||||
OWNER="projekte"
|
||||
DIST="trixie"
|
||||
COMPONENT="main"
|
||||
COMPONENT="${1:-stable}"
|
||||
case "$COMPONENT" in stable|testing) ;; *) echo "cleanup-old: unknown channel '$COMPONENT' (expected stable or testing)" >&2; exit 2 ;; esac
|
||||
BASE="https://git.netcell-it.de"
|
||||
|
||||
# Geschützte Versionen (nur relevant im stable-Kanal): jeder vorhandene
|
||||
# Git-Tag `v*` im Repo-Root. Leer wenn nicht im Repo ausgeführt oder im
|
||||
# testing-Kanal (dort gibt es keine Tags/keinen Schutz).
|
||||
PROTECTED_VERSIONS=""
|
||||
if [ "$COMPONENT" = "stable" ]; then
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
PROTECTED_VERSIONS="$(git -C "$REPO_ROOT" tag --list 'v*' 2>/dev/null | sed 's/^v//' || true)"
|
||||
fi
|
||||
is_protected() {
|
||||
local v="$1"
|
||||
[ -z "$PROTECTED_VERSIONS" ] && return 1
|
||||
printf '%s\n' "$PROTECTED_VERSIONS" | grep -qx "$v"
|
||||
}
|
||||
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
if [ -r "$HOME/.gitea-token" ]; then
|
||||
GITEA_TOKEN="$(tr -d '\n' < "$HOME/.gitea-token")"
|
||||
@@ -37,6 +59,7 @@ TOK="$GITEA_TOKEN"
|
||||
PKGS=(
|
||||
"edgeguard:all"
|
||||
"edgeguard-api:amd64"
|
||||
"edgeguard-api:arm64"
|
||||
"edgeguard-ui:all"
|
||||
)
|
||||
|
||||
@@ -44,27 +67,37 @@ cleanup_pkg() {
|
||||
local pkg="$1"
|
||||
local arch="$2"
|
||||
|
||||
# Versionen sammeln. Gitea Package-API liefert flache Liste:
|
||||
# /api/v1/packages/{owner}?type=debian&q={name}
|
||||
# Versionen sammeln. Gitea Package-API liefert flache Liste über ALLE
|
||||
# Kanäle hinweg: /api/v1/packages/{owner}?type=debian&q={name}
|
||||
# Wir filtern client-seitig auf name (exact-match — Gitea-Query ist
|
||||
# leider substring-fuzzy) und sort -V (semver) absteigend.
|
||||
# leider substring-fuzzy) UND auf das Versionsformat des aktuellen
|
||||
# Kanals — stable=Semver (X.Y.Z), testing=datumsbasiert (YYYY.MM.DD.NN).
|
||||
# Die beiden Formate überlappen nie, das hält testing- und stable-
|
||||
# Versionen sauber getrennt, obwohl Gitea (Name,Version) global dedupliziert.
|
||||
local raw
|
||||
raw="$(curl -fsS -H "Authorization: token $TOK" \
|
||||
"$BASE/api/v1/packages/$OWNER?type=debian&q=$pkg&limit=1000")"
|
||||
|
||||
local ver_pattern
|
||||
if [ "$COMPONENT" = "stable" ]; then
|
||||
ver_pattern='^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
else
|
||||
ver_pattern='^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$'
|
||||
fi
|
||||
|
||||
local versions
|
||||
versions="$(printf '%s' "$raw" | jq -r --arg n "$pkg" \
|
||||
'.[] | select(.name==$n) | .version' | sort -V -r | awk '!seen[$0]++')"
|
||||
'.[] | select(.name==$n) | .version' | grep -E "$ver_pattern" | sort -V -r | awk '!seen[$0]++')"
|
||||
|
||||
if [ -z "$versions" ]; then
|
||||
echo " $pkg ($arch): no versions found, skip"
|
||||
echo " $pkg ($arch): no $COMPONENT versions found, skip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local count
|
||||
count="$(printf '%s\n' "$versions" | wc -l)"
|
||||
if [ "$count" -le "$KEEP" ]; then
|
||||
echo " $pkg ($arch): $count versions, ≤ keep=$KEEP, nothing to delete"
|
||||
echo " $pkg ($arch): $count $COMPONENT versions, ≤ keep=$KEEP, nothing to delete"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -72,10 +105,14 @@ cleanup_pkg() {
|
||||
to_delete="$(printf '%s\n' "$versions" | tail -n +$((KEEP+1)))"
|
||||
|
||||
local kept_count=$((count - $(printf '%s\n' "$to_delete" | wc -l)))
|
||||
echo " $pkg ($arch): $count versions total, keeping $kept_count newest, deleting $(printf '%s\n' "$to_delete" | wc -l)"
|
||||
echo " $pkg ($arch): $count $COMPONENT versions total, keeping $kept_count newest, considering $(printf '%s\n' "$to_delete" | wc -l) for deletion"
|
||||
|
||||
while IFS= read -r v; do
|
||||
[ -z "$v" ] && continue
|
||||
if is_protected "$v"; then
|
||||
echo " skip $pkg $v $arch (protected — git tag v$v exists)"
|
||||
continue
|
||||
fi
|
||||
# DELETE-Endpoint für Debian-Pakete:
|
||||
# /api/packages/{owner}/debian/pool/{dist}/{comp}/{name}/{version}/{arch}
|
||||
local url="$BASE/api/packages/$OWNER/debian/pool/$DIST/$COMPONENT/$pkg/$v/$arch"
|
||||
|
||||
@@ -11,16 +11,22 @@
|
||||
# Befund 2026-05-17 nach Gitea-Disk-Full-Vorfall.
|
||||
#
|
||||
# Aufruf:
|
||||
# ./publish.sh <version> <arch>
|
||||
# arch = amd64 | arm64. arm64 publiziert NUR edgeguard-api (das einzige
|
||||
# arch-spezifische Paket); amd64 publiziert alle drei.
|
||||
# ./publish.sh <version> <arch> [channel]
|
||||
# arch = amd64 | arm64. arm64 publiziert NUR edgeguard-api (das einzige
|
||||
# arch-spezifische Paket); amd64 publiziert alle drei.
|
||||
# channel = stable | testing (Default stable). Kanal-Modell wie enconf:
|
||||
# Suite=trixie fest, Komponente=Kanal. Wird von scripts/release.sh
|
||||
# gesetzt — direkter Aufruf (z.B. aus altem `make publish`) bleibt
|
||||
# ohne 3. Arg unverändert auf stable.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:?usage: publish.sh <version> <arch>}"
|
||||
ARCH="${2:?usage: publish.sh <version> <arch>}"
|
||||
VERSION="${1:?usage: publish.sh <version> <arch> [channel]}"
|
||||
ARCH="${2:?usage: publish.sh <version> <arch> [channel]}"
|
||||
CHANNEL="${3:-stable}"
|
||||
case "$CHANNEL" in stable|testing) ;; *) echo "publish: unknown channel '$CHANNEL' (expected stable or testing)" >&2; exit 2 ;; esac
|
||||
|
||||
BASE="https://git.netcell-it.de/api/packages/projekte/debian/pool/trixie/main/upload"
|
||||
BASE="https://git.netcell-it.de/api/packages/projekte/debian/pool/trixie/${CHANNEL}/upload"
|
||||
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
if [ -r "$HOME/.gitea-token" ]; then
|
||||
@@ -88,4 +94,4 @@ case "$ARCH" in
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "publish: ok ($VERSION/$ARCH)"
|
||||
echo "publish: ok ($VERSION/$ARCH/$CHANNEL)"
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#
|
||||
# curl -fsSL https://get.netcell-edgeguard.de | sudo bash
|
||||
#
|
||||
# Kanal wählen (Default: stable):
|
||||
# curl -fsSL https://get.netcell-edgeguard.de | EDGEGUARD_CHANNEL=testing sudo -E bash
|
||||
#
|
||||
# Supported: Debian 13 (Trixie), amd64 + arm64.
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -111,12 +114,14 @@ setup_repo() {
|
||||
curl -fsSL "https://git.netcell-it.de/api/packages/projekte/debian/repository.key" \
|
||||
-o /etc/apt/keyrings/nmg.asc
|
||||
fi
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nmg.asc] https://git.netcell-it.de/api/packages/projekte/debian trixie main" \
|
||||
# Kanal-Modell (wie enconf): Suite = OS-Codename, Komponente = Kanal.
|
||||
# Default stable; EDGEGUARD_CHANNEL=testing für Testing-Kanal.
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nmg.asc] https://git.netcell-it.de/api/packages/projekte/debian trixie ${EDGEGUARD_CHANNEL:-stable}" \
|
||||
> /etc/apt/sources.list.d/edgeguard.list
|
||||
|
||||
apt-get update -qq
|
||||
}
|
||||
step "Set up EdgeGuard apt repository" setup_repo
|
||||
step "Set up EdgeGuard apt repository (${EDGEGUARD_CHANNEL:-stable})" setup_repo
|
||||
|
||||
AVAILABLE=$(LC_ALL=C apt-cache policy edgeguard 2>/dev/null | awk '/Candidate:/ {print $2; exit}' || true)
|
||||
if [ -n "$AVAILABLE" ] && [ "$AVAILABLE" != "(none)" ]; then
|
||||
|
||||
193
scripts/release.sh
Executable file
193
scripts/release.sh
Executable file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
# EdgeGuard — Release erstellen (Testing-Push oder Stable-Promotion)
|
||||
#
|
||||
# Verwendung:
|
||||
# ./scripts/release.sh # Testing-Push: baut den aktuellen
|
||||
# # Stand, Version = datumsbasiert
|
||||
# # YYYY.MM.DD.NN, Upload → testing
|
||||
# ./scripts/release.sh stable # Stable-Promotion: baut den
|
||||
# # aktuellen Stand unter der
|
||||
# # nächsten Patch-Version (VERSION-
|
||||
# # Datei +1), Upload → stable,
|
||||
# # VERSION-Commit + Git-Tag v<version>
|
||||
# ./scripts/release.sh stable 1.4.0 # Stable mit expliziter Version
|
||||
#
|
||||
# Kanal-Modell 1:1 von enconf (netcell-webpanel) übernommen — Suite=trixie
|
||||
# fest, Komponente=Kanal (stable/testing). An EdgeGuard angepasst:
|
||||
# - Gates laufen über die bestehende Go-Quality-Baseline (`make deb` ruft
|
||||
# `release-check` + `management-ui`-tsc automatisch auf) statt eigener
|
||||
# Preflight-Schritte.
|
||||
# - Kein Docs-/Changelog-/Checksum-/Marketing-Site-Deploy — dafür
|
||||
# existiert bei EdgeGuard keine Infrastruktur (siehe CLAUDE.md).
|
||||
# - Testing-Versionen sind datumsbasiert (wie enconf) DAMIT sie (a) bei
|
||||
# apt immer über jeder Stable-Semver-Version sortieren und (b) nie mit
|
||||
# einer Stable-Versionsnummer kollidieren — Gitea dedupliziert
|
||||
# (Name,Version) global über alle Kanäle hinweg.
|
||||
#
|
||||
# Voraussetzung: GITEA_TOKEN (env oder ~/.gitea-token) mit Package-
|
||||
# Upload/Delete/Query-Scope.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$REPO_DIR"
|
||||
|
||||
OWNER="projekte"
|
||||
GITEA_URL="https://git.netcell-it.de"
|
||||
KEEP="${KEEP:-10}"
|
||||
|
||||
log() { echo "[release] $*"; }
|
||||
warn() { echo " ⚠ $*" >&2; }
|
||||
abort() { echo "🛑 ABBRUCH: $*" >&2; exit 1; }
|
||||
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
if [ -r "$HOME/.gitea-token" ]; then
|
||||
GITEA_TOKEN="$(tr -d '\n' < "$HOME/.gitea-token")"
|
||||
else
|
||||
abort "GITEA_TOKEN fehlt (env oder ~/.gitea-token)"
|
||||
fi
|
||||
fi
|
||||
export GITEA_TOKEN
|
||||
|
||||
# `make ui` fällt bei fehlendem bun auf `npm install` zurück (Convenience
|
||||
# für schnelle lokale Builds) — das löst Dependencies gegen package.json
|
||||
# neu auf statt gegen das gepinnte management-ui/bun.lock, und hinterlässt
|
||||
# eine package-lock.json die nichts mit dem committeten Lockfile zu tun
|
||||
# hat. Für einen Release (Testing ODER Stable) ist das nicht akzeptabel —
|
||||
# beide Kanäle müssen reproduzierbar aus dem gepinnten Lockfile bauen.
|
||||
command -v bun >/dev/null 2>&1 || \
|
||||
abort "bun fehlt — Release-Builds müssen aus management-ui/bun.lock bauen, nicht aus dem npm-Fallback. Installieren: https://bun.sh/install"
|
||||
|
||||
# ─── Testing-Version: datumsbasiert YYYY.MM.DD.NN ──────────────────────
|
||||
# NN = laufende Nummer für den Tag, ermittelt aus den bereits im Registry
|
||||
# vorhandenen Versionen des Meta-Pakets (edgeguard) mit dem heutigen
|
||||
# Datumspräfix. Erster Release des Tages -> .01.
|
||||
next_testing_version() {
|
||||
local today
|
||||
today="$(date -u +%Y.%m.%d)"
|
||||
local raw
|
||||
raw="$(curl -fsS -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$GITEA_URL/api/v1/packages/$OWNER?type=debian&q=edgeguard&limit=1000" 2>/dev/null || echo '[]')"
|
||||
local last_n
|
||||
last_n="$(printf '%s' "$raw" | jq -r --arg n "edgeguard" --arg pfx "${today}." \
|
||||
'.[] | select(.name==$n) | .version | select(startswith($pfx))' \
|
||||
| sed "s/^${today}\.//" | sort -n | tail -1)"
|
||||
printf '%s.%02d' "$today" "$(( ${last_n:-0} + 1 ))"
|
||||
}
|
||||
|
||||
# ─── Stable-Version: Patch-Bump der VERSION-Datei (Default) ────────────
|
||||
bump_patch() {
|
||||
local major minor patch
|
||||
IFS='.' read -r major minor patch <<< "$1"
|
||||
echo "${major}.${minor}.$((patch + 1))"
|
||||
}
|
||||
|
||||
# ─── Alte Version im Ziel-Kanal ersetzen — Upload derselben Nummer würde
|
||||
# sonst mit 409 den alten Inhalt behalten (Gitea dedupliziert (Name,
|
||||
# Version,Arch,Kanal-Pool) — best-effort, 404 ist der Normalfall). ──────
|
||||
delete_before_upload() {
|
||||
local channel="$1" version="$2" name="$3" arch="$4"
|
||||
local url="$GITEA_URL/api/packages/$OWNER/debian/pool/trixie/$channel/$name/$version/$arch"
|
||||
curl -sS -o /dev/null -w '' -X DELETE -H "Authorization: token $GITEA_TOKEN" "$url" || true
|
||||
}
|
||||
|
||||
replace_in_channel() {
|
||||
local channel="$1" version="$2"
|
||||
delete_before_upload "$channel" "$version" edgeguard-api amd64
|
||||
delete_before_upload "$channel" "$version" edgeguard-api arm64
|
||||
delete_before_upload "$channel" "$version" edgeguard-ui all
|
||||
delete_before_upload "$channel" "$version" edgeguard all
|
||||
}
|
||||
|
||||
# ─── verify_channel_debs — Health-Gate wie enconf verify_stable_debs ───
|
||||
# Holt den Packages-Index eines Kanals und prüft für JEDE dort
|
||||
# referenzierte .deb, ob sie unter ihrer Pool-URL wirklich mit HTTP 200
|
||||
# abrufbar ist (nicht nur ob der Index existiert — Gitea regeneriert den
|
||||
# Index nicht immer zuverlässig nach einem Delete).
|
||||
verify_channel_debs() {
|
||||
local channel="$1"
|
||||
log "verify_channel_debs($channel): jede indexierte .deb muss abrufbar sein..."
|
||||
local files f code bad=0 checked=0
|
||||
files="$(curl -sk -H "Authorization: token $GITEA_TOKEN" \
|
||||
"$GITEA_URL/api/packages/$OWNER/debian/dists/trixie/$channel/binary-amd64/Packages" 2>/dev/null \
|
||||
| awk '/^Filename:/{print $2}')"
|
||||
if [ -z "$files" ]; then
|
||||
warn "$channel: Packages-Index leer/nicht erreichbar — übersprungen"
|
||||
return 0
|
||||
fi
|
||||
while IFS= read -r f; do
|
||||
[ -z "$f" ] && continue
|
||||
checked=$((checked + 1))
|
||||
code="$(curl -sk -o /dev/null -w '%{http_code}' \
|
||||
-H "Authorization: token $GITEA_TOKEN" "$GITEA_URL/api/packages/$OWNER/debian/$f")"
|
||||
if [ "$code" != "200" ]; then
|
||||
warn "$channel: $f -> HTTP $code"
|
||||
bad=$((bad + 1))
|
||||
fi
|
||||
done <<< "$files"
|
||||
[ "$bad" -eq 0 ] || abort "$channel: $bad von $checked .deb(s) im Index nicht abrufbar — Registry inkonsistent."
|
||||
log "$channel: $checked .deb(s) verifiziert, alle abrufbar."
|
||||
}
|
||||
|
||||
# ─── Testing-Push ───────────────────────────────────────────────────────
|
||||
testing_push() {
|
||||
local new_version
|
||||
new_version="$(next_testing_version)"
|
||||
log "[testing 1/3] Gates + Build $new_version (make deb — release-check, amd64+arm64+ui)..."
|
||||
make VERSION="$new_version" deb
|
||||
|
||||
log "[testing 2/3] Alte Version im testing-Kanal ersetzen + hochladen..."
|
||||
replace_in_channel testing "$new_version"
|
||||
./scripts/apt-repo/publish.sh "$new_version" amd64 testing
|
||||
./scripts/apt-repo/publish.sh "$new_version" arm64 testing
|
||||
|
||||
log "[testing 3/3] Cleanup (keep last $KEEP) + Verify..."
|
||||
KEEP="$KEEP" ./scripts/apt-repo/cleanup-old.sh testing
|
||||
verify_channel_debs testing
|
||||
|
||||
log "✅ Testing-Release $new_version veröffentlicht."
|
||||
}
|
||||
|
||||
# ─── Stable-Promotion ───────────────────────────────────────────────────
|
||||
promote_stable() {
|
||||
local explicit_version="${1:-}"
|
||||
local current_version new_version
|
||||
current_version="$(cat "$REPO_DIR/VERSION")"
|
||||
new_version="${explicit_version:-$(bump_patch "$current_version")}"
|
||||
|
||||
git -C "$REPO_DIR" rev-parse "v$new_version" >/dev/null 2>&1 && \
|
||||
abort "Git-Tag v$new_version existiert bereits."
|
||||
# --untracked-files=no: nur versionierte Änderungen blocken. Das Repo
|
||||
# kann fremde, noch nicht committete Arbeit in eigenen Verzeichnissen
|
||||
# liegen haben (untracked) — die geht ein Stable-Release nichts an.
|
||||
[ -n "$(git -C "$REPO_DIR" status --porcelain --untracked-files=no)" ] && \
|
||||
abort "Uncommittete Änderungen an versionierten Dateien — commit/stash erst, dann Stable-Release."
|
||||
|
||||
log "[stable 1/4] Gates + Build $new_version (make deb — release-check, amd64+arm64+ui)..."
|
||||
make VERSION="$new_version" deb
|
||||
|
||||
log "[stable 2/4] Alte Version im stable-Kanal ersetzen + hochladen..."
|
||||
replace_in_channel stable "$new_version"
|
||||
./scripts/apt-repo/publish.sh "$new_version" amd64 stable
|
||||
./scripts/apt-repo/publish.sh "$new_version" arm64 stable
|
||||
|
||||
log "[stable 3/4] VERSION-Datei setzen + Commit + Git-Tag..."
|
||||
printf '%s' "$new_version" > "$REPO_DIR/VERSION"
|
||||
git -C "$REPO_DIR" add VERSION
|
||||
git -C "$REPO_DIR" commit -m "chore(release): v$new_version stable"
|
||||
git -C "$REPO_DIR" tag "v$new_version"
|
||||
|
||||
log "[stable 4/4] Cleanup (keep last $KEEP) + Verify..."
|
||||
KEEP="$KEEP" ./scripts/apt-repo/cleanup-old.sh stable
|
||||
verify_channel_debs stable
|
||||
|
||||
log "✅ Stable-Release v$new_version fertig."
|
||||
log " Push nicht vergessen: git push origin main --tags"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
stable) promote_stable "${2:-}" ;;
|
||||
"") testing_push ;;
|
||||
*) abort "unbekanntes Argument '$1' (erwartet: 'stable' oder kein Argument)" ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user