From 255c7c23d4b9f3d538119bdbfc42bc2552edc244 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 9 Jul 2026 23:20:35 +0100 Subject: [PATCH] Modernize Unified Agent lifecycle and platform support --- .github/workflows/build-release-candidate.yml | 172 ++ .github/workflows/create-release.yml | 1 + .github/workflows/unified-agent-native.yml | 145 ++ Dockerfile | 2 +- cmd/pulse-agent/health_test.go | 38 + cmd/pulse-agent/main.go | 104 +- cmd/pulse-agent/main_test.go | 5 +- cmd/pulse-agent/runtime_health.go | 148 ++ cmd/pulse-agent/service_windows.go | 110 +- deploy/provider-msp/Dockerfile.control-plane | 2 +- .../UNIFIED_AGENT_PLATFORM_SUPPORT.md | 60 + .../v6/internal/subsystems/agent-lifecycle.md | 40 + .../v6/internal/subsystems/api-contracts.md | 10 + .../subsystems/deployment-installability.md | 5 +- .../v6/internal/subsystems/monitoring.md | 10 + .../internal/subsystems/security-privacy.md | 8 + .../internal/subsystems/storage-recovery.md | 8 + frontend-modern/package-lock.json | 1561 +++++++++-------- frontend-modern/src/api/connections.ts | 27 +- .../__tests__/connectionsTableModel.test.ts | 83 + .../__tests__/settingsArchitecture.test.ts | 10 + .../Settings/connectionsTableModel.ts | 64 +- frontend-modern/src/types/api.ts | 27 + go.mod | 83 +- go.sum | 235 +-- internal/agenttls/config.go | 9 + internal/agenttls/config_test.go | 9 + internal/agentupdate/coverage_test.go | 12 +- internal/agentupdate/update.go | 133 +- internal/agentupdate/update_http_test.go | 51 +- internal/agentupdate/update_test.go | 2 +- internal/api/connections_aggregator.go | 80 +- internal/api/connections_aggregator_test.go | 40 +- internal/api/connections_types.go | 59 +- internal/dockeragent/agent.go | 97 +- internal/dockeragent/agent_collect_test.go | 26 + internal/dockeragent/agent_internal_test.go | 6 +- internal/hostagent/agent.go | 68 +- internal/hostagent/agent_metrics_test.go | 25 +- internal/hostagent/agent_new_test.go | 36 +- internal/kubernetesagent/agent.go | 11 +- internal/kubernetesagent/agent_new_test.go | 24 + internal/models/converters.go | 38 + internal/models/models.go | 95 +- internal/models/models_frontend.go | 7 +- internal/monitoring/monitor_agents.go | 112 ++ .../monitoring/monitor_host_agents_test.go | 57 + internal/remoteconfig/client.go | 4 +- .../remoteconfig/client_additional_test.go | 4 +- pkg/agents/host/report.go | 52 +- scripts/.go-version | 2 +- scripts/build-release.sh | 25 +- scripts/install-go-toolchain.sh | 2 +- scripts/install.ps1 | 35 +- scripts/install.sh | 81 +- .../installtests/build_release_assets_test.go | 27 +- scripts/installtests/install_ps1_test.go | 23 +- scripts/installtests/install_sh_test.go | 26 +- .../installtests/provider_msp_deploy_test.go | 2 +- scripts/tests/test-hot-dev-runtime.sh | 4 +- 60 files changed, 3086 insertions(+), 1156 deletions(-) create mode 100644 .github/workflows/unified-agent-native.yml create mode 100644 cmd/pulse-agent/runtime_health.go create mode 100644 docs/release-control/v6/internal/UNIFIED_AGENT_PLATFORM_SUPPORT.md diff --git a/.github/workflows/build-release-candidate.yml b/.github/workflows/build-release-candidate.yml index 3746dddac2..0f55d6a152 100644 --- a/.github/workflows/build-release-candidate.yml +++ b/.github/workflows/build-release-candidate.yml @@ -7,6 +7,11 @@ on: description: 'Version number without the leading v' required: true type: string + require_platform_signing: + description: 'Require Developer ID/notarized macOS and Authenticode Windows agent binaries' + required: false + default: false + type: boolean outputs: artifact_name: description: 'Immutable release candidate artifact name' @@ -19,8 +24,159 @@ permissions: contents: read jobs: + sign-macos-agent: + name: Sign and Notarize macOS Agent + if: ${{ inputs.require_platform_signing }} + runs-on: macos-15 + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + cache: true + + - name: Build, sign, and notarize agent binaries + shell: bash + env: + APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_P12_BASE64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_IDENTITY }} + APPLE_NOTARY_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_KEY_P8_BASE64 }} + APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }} + APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }} + PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }} + run: | + set -euo pipefail + for name in \ + APPLE_CERTIFICATE_P12_BASE64 \ + APPLE_CERTIFICATE_PASSWORD \ + APPLE_SIGNING_IDENTITY \ + APPLE_NOTARY_KEY_P8_BASE64 \ + APPLE_NOTARY_KEY_ID \ + APPLE_NOTARY_ISSUER_ID \ + PULSE_UPDATE_SIGNING_PUBLIC_KEY; do + test -n "${!name:-}" || { echo "::error::Missing required ${name}."; exit 1; } + done + + mkdir -p native-agent-binaries + python3 - <<'PY' + import base64, os + from pathlib import Path + Path('developer-id.p12').write_bytes(base64.b64decode(os.environ['APPLE_CERTIFICATE_P12_BASE64'])) + Path('AuthKey.p8').write_bytes(base64.b64decode(os.environ['APPLE_NOTARY_KEY_P8_BASE64'])) + PY + + keychain="$RUNNER_TEMP/pulse-signing.keychain-db" + keychain_password="$(openssl rand -hex 24)" + security create-keychain -p "$keychain_password" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$keychain_password" "$keychain" + security import developer-id.p12 -k "$keychain" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" + security list-keychains -d user -s "$keychain" + + ldflags="$(./scripts/release_ldflags.sh agent \ + --version "v${{ inputs.version }}" \ + --update-public-keys "$PULSE_UPDATE_SIGNING_PUBLIC_KEY")" + for arch in amd64 arm64; do + output="native-agent-binaries/pulse-agent-darwin-${arch}" + GOOS=darwin GOARCH="$arch" CGO_ENABLED=0 \ + go build -buildvcs=false -trimpath -ldflags="$ldflags" -o "$output" ./cmd/pulse-agent + codesign --force --timestamp --options runtime --sign "$APPLE_SIGNING_IDENTITY" "$output" + codesign --verify --deep --strict --verbose=2 "$output" + done + + ditto -c -k --keepParent native-agent-binaries pulse-agent-macos-notarization.zip + xcrun notarytool submit pulse-agent-macos-notarization.zip \ + --key AuthKey.p8 \ + --key-id "$APPLE_NOTARY_KEY_ID" \ + --issuer "$APPLE_NOTARY_ISSUER_ID" \ + --wait + for binary in native-agent-binaries/pulse-agent-darwin-*; do + spctl --assess --type execute --verbose=2 "$binary" + done + + - name: Upload signed macOS binaries + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: signed-macos-agent-${{ github.sha }}-${{ inputs.version }} + path: native-agent-binaries/ + if-no-files-found: error + retention-days: 1 + compression-level: 0 + + sign-windows-agent: + name: Authenticode Sign Windows Agent + if: ${{ inputs.require_platform_signing }} + runs-on: windows-2025 + timeout-minutes: 25 + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + cache: true + + - name: Build and sign agent binaries + shell: pwsh + env: + WINDOWS_CERTIFICATE_PFX_BASE64: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PFX_BASE64 }} + WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CODE_SIGNING_CERTIFICATE_PASSWORD }} + PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }} + run: | + $ErrorActionPreference = 'Stop' + foreach ($name in @('WINDOWS_CERTIFICATE_PFX_BASE64', 'WINDOWS_CERTIFICATE_PASSWORD', 'PULSE_UPDATE_SIGNING_PUBLIC_KEY')) { + if ([string]::IsNullOrWhiteSpace((Get-Item "Env:$name").Value)) { + throw "Missing required $name." + } + } + + New-Item -ItemType Directory -Path native-agent-binaries -Force | Out-Null + $pfxPath = Join-Path $env:RUNNER_TEMP 'pulse-code-signing.pfx' + [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_PFX_BASE64)) + $password = ConvertTo-SecureString $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force + $certificate = Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation Cert:\CurrentUser\My -Password $password -Exportable:$false + if ($null -eq $certificate) { throw 'Failed to import Windows code-signing certificate.' } + + $signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Filter signtool.exe -Recurse | + Sort-Object FullName -Descending | + Select-Object -First 1 -ExpandProperty FullName + if ([string]::IsNullOrWhiteSpace($signtool)) { throw 'signtool.exe was not found.' } + + $ldflags = & bash ./scripts/release_ldflags.sh agent --version "v${{ inputs.version }}" --update-public-keys $env:PULSE_UPDATE_SIGNING_PUBLIC_KEY + foreach ($arch in @('amd64', 'arm64', '386')) { + $env:GOOS = 'windows' + $env:GOARCH = $arch + $env:CGO_ENABLED = '0' + $output = "native-agent-binaries/pulse-agent-windows-$arch.exe" + go build -buildvcs=false -trimpath -ldflags="$ldflags" -o $output ./cmd/pulse-agent + if ($LASTEXITCODE -ne 0) { throw "Go build failed for Windows $arch." } + & $signtool sign /sha1 $certificate.Thumbprint /fd SHA256 /td SHA256 /tr http://timestamp.digicert.com $output + if ($LASTEXITCODE -ne 0) { throw "Authenticode signing failed for Windows $arch." } + & $signtool verify /pa /v $output + if ($LASTEXITCODE -ne 0) { throw "Authenticode verification failed for Windows $arch." } + } + + - name: Upload signed Windows binaries + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: signed-windows-agent-${{ github.sha }}-${{ inputs.version }} + path: native-agent-binaries/ + if-no-files-found: error + retention-days: 1 + compression-level: 0 + build: name: Build and Validate Signed Candidate + needs: [sign-macos-agent, sign-windows-agent] + if: ${{ always() && (!inputs.require_platform_signing || (needs.sign-macos-agent.result == 'success' && needs.sign-windows-agent.result == 'success')) }} runs-on: ubuntu-24.04 timeout-minutes: 60 outputs: @@ -82,12 +238,28 @@ jobs: tar -xzf "${TMP_DIR}/${SYFT_ARCHIVE}" -C "${TMP_DIR}" syft install -m 0755 "${TMP_DIR}/syft" /usr/local/bin/syft + - name: Download signed macOS binaries + if: ${{ inputs.require_platform_signing }} + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: signed-macos-agent-${{ github.sha }}-${{ inputs.version }} + path: native-agent-binaries + + - name: Download signed Windows binaries + if: ${{ inputs.require_platform_signing }} + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: signed-windows-agent-${{ github.sha }}-${{ inputs.version }} + path: native-agent-binaries + - name: Build signed release candidate run: ./scripts/build-release.sh "${{ inputs.version }}" env: PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }} PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }} PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }} + PULSE_REQUIRE_PLATFORM_SIGNING: ${{ inputs.require_platform_signing }} + PULSE_AGENT_NATIVE_BINARIES_DIR: ${{ inputs.require_platform_signing && format('{0}/native-agent-binaries', github.workspace) || '' }} - name: Validate installer signing key pins env: diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 332b5a05f4..82bea6f1c6 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -218,6 +218,7 @@ jobs: secrets: inherit with: version: ${{ needs.prepare.outputs.version }} + require_platform_signing: true # Frontend checks run in parallel with backend tests frontend_checks: diff --git a/.github/workflows/unified-agent-native.yml b/.github/workflows/unified-agent-native.yml new file mode 100644 index 0000000000..fc89332d75 --- /dev/null +++ b/.github/workflows/unified-agent-native.yml @@ -0,0 +1,145 @@ +name: Unified Agent Native Verification + +on: + push: + branches: [main] + paths: + - '.github/workflows/unified-agent-native.yml' + - 'cmd/pulse-agent/**' + - 'internal/agenttls/**' + - 'internal/agentupdate/**' + - 'internal/dockeragent/**' + - 'internal/hostagent/**' + - 'internal/kubernetesagent/**' + - 'internal/remoteconfig/**' + - 'pkg/agents/**' + - 'scripts/install.sh' + - 'scripts/install.ps1' + - 'scripts/installtests/**' + - 'go.mod' + - 'go.sum' + pull_request: + branches: [main] + paths: + - '.github/workflows/unified-agent-native.yml' + - 'cmd/pulse-agent/**' + - 'internal/agenttls/**' + - 'internal/agentupdate/**' + - 'internal/dockeragent/**' + - 'internal/hostagent/**' + - 'internal/kubernetesagent/**' + - 'internal/remoteconfig/**' + - 'pkg/agents/**' + - 'scripts/install.sh' + - 'scripts/install.ps1' + - 'scripts/installtests/**' + - 'go.mod' + - 'go.sum' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + native-agent: + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + - name: Linux x64 + runner: ubuntu-24.04 + unix: true + - name: Linux ARM64 + runner: ubuntu-24.04-arm + unix: true + - name: macOS ARM64 + runner: macos-15 + unix: true + - name: macOS Intel + runner: macos-15-intel + unix: true + - name: Windows x64 + runner: windows-2025 + unix: false + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + cache: true + + - name: Test native agent runtime + run: >- + go test + ./cmd/pulse-agent + ./internal/agenttls + ./internal/agentupdate + ./internal/dockeragent + ./internal/hostagent + ./internal/kubernetesagent + ./internal/remoteconfig + + - name: Test native Unix installer contracts + if: matrix.unix + run: go test ./scripts/installtests + + - name: Build and execute native Unix agent + if: matrix.unix + shell: bash + run: | + set -euo pipefail + go build -o pulse-agent-native ./cmd/pulse-agent + ./pulse-agent-native --version + ./pulse-agent-native --self-test + + - name: Build and execute native Windows agent + if: ${{ !matrix.unix }} + shell: pwsh + run: | + go build -o pulse-agent-native.exe ./cmd/pulse-agent + .\pulse-agent-native.exe --version + .\pulse-agent-native.exe --self-test + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path 'scripts/install.ps1'), + [ref]$null, + [ref]$errors + ) > $null + if ($errors.Count) { + $errors | ForEach-Object { Write-Error $_.ToString() } + exit 1 + } + + freebsd-build-contract: + name: FreeBSD cross-build contract + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + cache: true + + - name: Compile and validate FreeBSD binaries + shell: bash + run: | + set -euo pipefail + for arch in amd64 arm64; do + GOOS=freebsd GOARCH="$arch" CGO_ENABLED=0 \ + go build -o "pulse-agent-freebsd-${arch}" ./cmd/pulse-agent + magic="$(od -An -tx1 -N4 "pulse-agent-freebsd-${arch}" | tr -d ' \n')" + test "$magic" = "7f454c46" + done + GOOS=freebsd GOARCH=amd64 go test -c -o agentupdate-freebsd.test ./internal/agentupdate diff --git a/Dockerfile b/Dockerfile index 5a943500e0..414c5e9d16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ RUN --mount=type=cache,id=pulse-npm-cache,target=/root/.npm \ # Build stage for Go backend # Force amd64 platform - Go cross-compiles for all targets anyway, # and this avoids slow QEMU emulation during multi-arch builds -FROM --platform=linux/amd64 golang:1.25.11-alpine@sha256:8d95af53d0d58e1759ddb4028285d9b1239067e4fbf4f544618cad0f60fbc354 AS backend-builder +FROM --platform=linux/amd64 golang:1.26.5-alpine@sha256:0178a641fbb4858c5f1b48e34bdaabe0350a330a1b1149aabd498d0699ff5fb2 AS backend-builder ARG BUILD_AGENT ARG VERSION diff --git a/cmd/pulse-agent/health_test.go b/cmd/pulse-agent/health_test.go index 615f1b6f78..76c8e01c6c 100644 --- a/cmd/pulse-agent/health_test.go +++ b/cmd/pulse-agent/health_test.go @@ -1,6 +1,8 @@ package main import ( + "encoding/json" + "errors" "net/http" "net/http/httptest" "sync/atomic" @@ -39,3 +41,39 @@ func TestHealthHandler_ReadyzDependsOnReadyFlag(t *testing.T) { t.Fatalf("expected 200, got %d", rec.Code) } } + +func TestHealthHandler_ReadyzExplainsModuleReadiness(t *testing.T) { + var ready atomic.Bool + runtimeStatus := newRuntimeHealth(&ready, map[string]bool{ + "host": true, + "docker": true, + "kubernetes": false, + }) + h := healthHandler(&ready, runtimeStatus) + req := httptest.NewRequest(http.MethodGet, "/readyz", nil) + + runtimeStatus.setState("host", moduleStateRunning, nil) + runtimeStatus.setState("docker", moduleStateRetrying, errors.New("docker socket unavailable")) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("expected 503 while docker retries, got %d", rec.Code) + } + var waiting runtimeHealthSnapshot + if err := json.Unmarshal(rec.Body.Bytes(), &waiting); err != nil { + t.Fatalf("decode readiness response: %v", err) + } + if waiting.Ready || len(waiting.Modules) != 3 { + t.Fatalf("waiting snapshot = %+v", waiting) + } + if waiting.Modules[0].Name != "docker" || waiting.Modules[0].State != moduleStateRetrying || waiting.Modules[0].LastError != "docker socket unavailable" { + t.Fatalf("docker readiness evidence = %+v", waiting.Modules[0]) + } + + runtimeStatus.setState("docker", moduleStateRunning, nil) + rec = httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusOK || !ready.Load() { + t.Fatalf("expected ready after enabled modules run, status=%d ready=%v body=%s", rec.Code, ready.Load(), rec.Body.String()) + } +} diff --git a/cmd/pulse-agent/main.go b/cmd/pulse-agent/main.go index cd2337a818..49078cbb66 100644 --- a/cmd/pulse-agent/main.go +++ b/cmd/pulse-agent/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "errors" "flag" "fmt" @@ -28,6 +29,7 @@ import ( "github.com/rcourtman/pulse-go-rewrite/internal/kubernetesagent" "github.com/rcourtman/pulse-go-rewrite/internal/remoteconfig" "github.com/rcourtman/pulse-go-rewrite/internal/utils" + agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host" "github.com/rs/zerolog" gohost "github.com/shirou/gopsutil/v4/host" "golang.org/x/sync/errgroup" @@ -46,6 +48,16 @@ var ( Name: "pulse_agent_up", Help: "Whether the Pulse agent is running (1 = up, 0 = down)", }) + + agentModuleEnabled = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "pulse_agent_module_enabled", + Help: "Whether a Pulse Unified Agent module is configured to run (1 = enabled, 0 = disabled)", + }, []string{"module"}) + + agentModuleReady = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "pulse_agent_module_ready", + Help: "Whether a configured Pulse Unified Agent module initialized successfully (1 = ready, 0 = not ready)", + }, []string{"module"}) ) // Runnable is an interface for agents that can be run @@ -246,6 +258,14 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { if len(settings) > 0 { applyRemoteSettings(&cfg, settings, &logger) } + if remoteconfig.HasAppliedDesiredConfig(commandsEnabled, settings) { + metadata, metadataErr := remoteconfig.BuildDesiredConfigMetadata(commandsEnabled, settings) + if metadataErr != nil { + logger.Warn().Err(metadataErr).Msg("Failed to derive applied managed configuration fingerprint") + } else { + cfg.AppliedConfig = &agentshost.ConfigFingerprint{Version: metadata.Version, Hash: metadata.Hash} + } + } } } @@ -260,6 +280,21 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { g, ctx := errgroup.WithContext(ctx) + // Resolve automatic runtime selection before publishing startup and + // readiness state so the process never claims a module set it did not + // actually attempt to start. + if !cfg.EnableDocker && !cfg.DockerConfigured { + if _, err := lookPath("docker"); err == nil { + logger.Info().Msg("Auto-detected Docker binary, enabling Docker monitoring") + cfg.EnableDocker = true + } else if _, err := lookPath("podman"); err == nil { + logger.Info().Msg("Auto-detected Podman binary, enabling Docker monitoring") + cfg.EnableDocker = true + } else { + logger.Debug().Msg("Docker/Podman not found, skipping Docker monitoring") + } + } + logger.Info(). Str("version", Version). Str("pulse_url", cfg.PulseURL). @@ -281,8 +316,13 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { // 6. Start Health/Metrics Server var ready atomic.Bool + runtimeStatus := newRuntimeHealth(&ready, map[string]bool{ + "host": cfg.EnableHost, + "docker": cfg.EnableDocker, + "kubernetes": cfg.EnableKubernetes, + }) if cfg.HealthAddr != "" { - startHealthServer(ctx, cfg.HealthAddr, &ready, &logger) + startHealthServer(ctx, cfg.HealthAddr, &ready, &logger, runtimeStatus) } // 7. Start Auto-Updater @@ -330,6 +370,9 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { StateDir: cfg.StateDir, ReportIP: cfg.ReportIP, DisableCeph: cfg.DisableCeph, + AppliedConfig: cfg.AppliedConfig, + UpdateStatus: updater.Snapshot, + ModuleStatus: runtimeStatus.moduleStatuses, } agent, err := newHostAgent(hostCfg) @@ -339,6 +382,7 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { if applier, ok := agent.(RemoteConfigApplier); ok { remoteConfigAppliers = append(remoteConfigAppliers, applier) } + runtimeStatus.setState("host", moduleStateRunning, nil) g.Go(func() error { logger.Info().Msg("Host agent module started") @@ -346,20 +390,6 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { }) } - // Auto-detect Docker/Podman if not explicitly configured - if !cfg.EnableDocker && !cfg.DockerConfigured { - // Check for docker binary - if _, err := lookPath("docker"); err == nil { - logger.Info().Msg("Auto-detected Docker binary, enabling Docker monitoring") - cfg.EnableDocker = true - } else if _, err := lookPath("podman"); err == nil { - logger.Info().Msg("Auto-detected Podman binary, enabling Docker monitoring") - cfg.EnableDocker = true - } else { - logger.Debug().Msg("Docker/Podman not found, skipping Docker monitoring") - } - } - // 9. Start Docker / Podman module (if enabled) var dockerAgent RunnableCloser if cfg.EnableDocker { @@ -372,6 +402,8 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { AgentType: "unified", AgentVersion: Version, InsecureSkipVerify: cfg.InsecureSkipVerify, + CACertPath: cfg.CACertPath, + ServerFingerprint: cfg.ServerFingerprint, DisableAutoUpdate: cfg.DisableAutoUpdate, DisableUpdateChecks: cfg.DisableDockerUpdateChecks, Runtime: cfg.DockerRuntime, @@ -387,6 +419,7 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { dockerAgent, err = newDockerAgent(dockerCfg) if err != nil { + runtimeStatus.setState("docker", moduleStateRetrying, err) // Docker isn't available yet - start retry loop in background logger.Warn(). Err(err). @@ -398,6 +431,7 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { agent := initDockerWithRetry(ctx, dockerCfg, &logger) if agent != nil { dockerAgent = agent + runtimeStatus.setState("docker", moduleStateRunning, nil) logger.Info().Msg("Docker / Podman module started (after retry)") return agent.Run(ctx) } @@ -405,6 +439,7 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { return nil }) } else { + runtimeStatus.setState("docker", moduleStateRunning, nil) g.Go(func() error { logger.Info().Msg("Docker / Podman module started") return dockerAgent.Run(ctx) @@ -422,6 +457,8 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { AgentType: "unified", AgentVersion: Version, InsecureSkipVerify: cfg.InsecureSkipVerify, + CACertPath: cfg.CACertPath, + ServerFingerprint: cfg.ServerFingerprint, LogLevel: cfg.LogLevel, Logger: &logger, KubeconfigPath: cfg.KubeconfigPath, @@ -435,6 +472,7 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { agent, err := newKubeAgent(kubeCfg) if err != nil { + runtimeStatus.setState("kubernetes", moduleStateRetrying, err) logger.Warn(). Err(err). Str("component", "kubernetes_agent"). @@ -444,12 +482,14 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { g.Go(func() error { retried := initKubernetesWithRetry(ctx, kubeCfg, &logger) if retried != nil { + runtimeStatus.setState("kubernetes", moduleStateRunning, nil) logger.Info().Msg("Kubernetes agent module started (after retry)") return retried.Run(ctx) } return nil }) } else { + runtimeStatus.setState("kubernetes", moduleStateRunning, nil) g.Go(func() error { logger.Info().Msg("Kubernetes agent module started") return agent.Run(ctx) @@ -464,9 +504,6 @@ func run(ctx context.Context, args []string, getenv func(string) string) error { }) } - // Mark as ready after all agents started - ready.Store(true) - // 11. Wait for all agents to exit if err := g.Wait(); err != nil && err != context.Canceled { logger.Error().Err(err).Msg("Agent terminated with error") @@ -586,8 +623,12 @@ func cleanupDockerAgent(agent RunnableCloser, logger *zerolog.Logger) { } } -func healthHandler(ready *atomic.Bool) http.Handler { +func healthHandler(ready *atomic.Bool, runtimes ...*runtimeHealth) http.Handler { mux := http.NewServeMux() + var runtimeStatus *runtimeHealth + if len(runtimes) > 0 { + runtimeStatus = runtimes[0] + } // Liveness probe - always returns 200 if server is running mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { @@ -597,6 +638,15 @@ func healthHandler(ready *atomic.Bool) http.Handler { // Readiness probe - returns 200 only when agents are initialized mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { + if runtimeStatus != nil { + snapshot := runtimeStatus.snapshot() + w.Header().Set("Content-Type", "application/json") + if !snapshot.Ready { + w.WriteHeader(http.StatusServiceUnavailable) + } + _ = json.NewEncoder(w).Encode(snapshot) + return + } if ready.Load() { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) @@ -606,15 +656,24 @@ func healthHandler(ready *atomic.Bool) http.Handler { } }) + mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if runtimeStatus == nil { + _ = json.NewEncoder(w).Encode(runtimeHealthSnapshot{Ready: ready.Load()}) + return + } + _ = json.NewEncoder(w).Encode(runtimeStatus.snapshot()) + }) + // Prometheus metrics mux.Handle("/metrics", promhttp.Handler()) return mux } -func startHealthServer(ctx context.Context, addr string, ready *atomic.Bool, logger *zerolog.Logger) { +func startHealthServer(ctx context.Context, addr string, ready *atomic.Bool, logger *zerolog.Logger, runtimes ...*runtimeHealth) { srv := &http.Server{ Addr: addr, - Handler: healthHandler(ready), + Handler: healthHandler(ready, runtimes...), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 30 * time.Second, @@ -698,7 +757,8 @@ type Config struct { SelfTest bool // Perform self-test and exit // Health/metrics server - HealthAddr string + HealthAddr string + AppliedConfig *agentshost.ConfigFingerprint // Kubernetes KubeconfigPath string diff --git a/cmd/pulse-agent/main_test.go b/cmd/pulse-agent/main_test.go index 295adf71d4..dc6686ece6 100644 --- a/cmd/pulse-agent/main_test.go +++ b/cmd/pulse-agent/main_test.go @@ -1945,8 +1945,9 @@ func TestWindowsServiceRuntimeStartsHealthServer(t *testing.T) { required := []string{ `var ready atomic.Bool`, - `startHealthServer(ctx, ws.cfg.HealthAddr, &ready, &ws.logger)`, - `ready.Store(true)`, + `runtimeStatus := newRuntimeHealth(&ready`, + `startHealthServer(ctx, ws.cfg.HealthAddr, &ready, &ws.logger, runtimeStatus)`, + `runtimeStatus.setState("host", moduleStateRunning, nil)`, `agentUp.Set(1)`, `defer agentUp.Set(0)`, } diff --git a/cmd/pulse-agent/runtime_health.go b/cmd/pulse-agent/runtime_health.go new file mode 100644 index 0000000000..8c7cf7c998 --- /dev/null +++ b/cmd/pulse-agent/runtime_health.go @@ -0,0 +1,148 @@ +package main + +import ( + "sort" + "sync" + "sync/atomic" + "time" + + agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host" +) + +const ( + moduleStateDisabled = "disabled" + moduleStateStarting = "starting" + moduleStateRetrying = "retrying" + moduleStateRunning = "running" +) + +type moduleHealth struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + State string `json:"state"` + LastError string `json:"lastError,omitempty"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type runtimeHealthSnapshot struct { + Ready bool `json:"ready"` + Modules []moduleHealth `json:"modules"` +} + +type runtimeHealth struct { + mu sync.RWMutex + ready *atomic.Bool + modules map[string]moduleHealth + now func() time.Time +} + +func newRuntimeHealth(ready *atomic.Bool, enabled map[string]bool) *runtimeHealth { + r := &runtimeHealth{ + ready: ready, + modules: make(map[string]moduleHealth, len(enabled)), + now: func() time.Time { return time.Now().UTC() }, + } + for name, isEnabled := range enabled { + state := moduleStateDisabled + if isEnabled { + state = moduleStateStarting + } + r.modules[name] = moduleHealth{ + Name: name, + Enabled: isEnabled, + State: state, + UpdatedAt: r.now(), + } + agentModuleEnabled.WithLabelValues(name).Set(boolGauge(isEnabled)) + } + r.reconcileReadyLocked() + return r +} + +func (r *runtimeHealth) setEnabled(name string, enabled bool) { + if r == nil { + return + } + r.mu.Lock() + defer r.mu.Unlock() + state := moduleStateDisabled + if enabled { + state = moduleStateStarting + } + r.modules[name] = moduleHealth{Name: name, Enabled: enabled, State: state, UpdatedAt: r.now()} + agentModuleEnabled.WithLabelValues(name).Set(boolGauge(enabled)) + r.reconcileReadyLocked() +} + +func (r *runtimeHealth) setState(name, state string, err error) { + if r == nil { + return + } + r.mu.Lock() + defer r.mu.Unlock() + module := r.modules[name] + module.Name = name + module.Enabled = state != moduleStateDisabled + module.State = state + module.LastError = "" + if err != nil { + module.LastError = err.Error() + } + module.UpdatedAt = r.now() + r.modules[name] = module + r.reconcileReadyLocked() +} + +func (r *runtimeHealth) reconcileReadyLocked() { + ready := true + for _, module := range r.modules { + moduleReady := module.Enabled && module.State == moduleStateRunning + agentModuleReady.WithLabelValues(module.Name).Set(boolGauge(moduleReady)) + if module.Enabled && module.State != moduleStateRunning { + ready = false + } + } + if r.ready != nil { + r.ready.Store(ready) + } +} + +func boolGauge(value bool) float64 { + if value { + return 1 + } + return 0 +} + +func (r *runtimeHealth) snapshot() runtimeHealthSnapshot { + if r == nil { + return runtimeHealthSnapshot{} + } + r.mu.RLock() + defer r.mu.RUnlock() + modules := make([]moduleHealth, 0, len(r.modules)) + ready := true + for _, module := range r.modules { + modules = append(modules, module) + if module.Enabled && module.State != moduleStateRunning { + ready = false + } + } + sort.Slice(modules, func(i, j int) bool { return modules[i].Name < modules[j].Name }) + return runtimeHealthSnapshot{Ready: ready, Modules: modules} +} + +func (r *runtimeHealth) moduleStatuses() []agentshost.ModuleStatus { + snapshot := r.snapshot() + statuses := make([]agentshost.ModuleStatus, 0, len(snapshot.Modules)) + for _, module := range snapshot.Modules { + statuses = append(statuses, agentshost.ModuleStatus{ + Name: module.Name, + Enabled: module.Enabled, + State: module.State, + LastError: module.LastError, + UpdatedAt: module.UpdatedAt, + }) + } + return statuses +} diff --git a/cmd/pulse-agent/service_windows.go b/cmd/pulse-agent/service_windows.go index b8df1f4e18..16cef61aab 100644 --- a/cmd/pulse-agent/service_windows.go +++ b/cmd/pulse-agent/service_windows.go @@ -12,6 +12,8 @@ import ( "github.com/rcourtman/pulse-go-rewrite/internal/agentupdate" "github.com/rcourtman/pulse-go-rewrite/internal/dockeragent" "github.com/rcourtman/pulse-go-rewrite/internal/hostagent" + "github.com/rcourtman/pulse-go-rewrite/internal/kubernetesagent" + "github.com/rcourtman/pulse-go-rewrite/internal/remoteconfig" "github.com/rs/zerolog" "golang.org/x/sync/errgroup" "golang.org/x/sys/windows/svc" @@ -50,9 +52,15 @@ func (ws *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, cha defer agentUp.Set(0) var ready atomic.Bool + runtimeStatus := newRuntimeHealth(&ready, map[string]bool{ + "host": ws.cfg.EnableHost, + "docker": ws.cfg.EnableDocker, + "kubernetes": ws.cfg.EnableKubernetes, + }) if ws.cfg.HealthAddr != "" { - startHealthServer(ctx, ws.cfg.HealthAddr, &ready, &ws.logger) + startHealthServer(ctx, ws.cfg.HealthAddr, &ready, &ws.logger, runtimeStatus) } + remoteConfigAppliers := make([]RemoteConfigApplier, 0, 1) // Start Auto-Updater updater := newUpdater(agentupdate.Config{ @@ -91,8 +99,10 @@ func (ws *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, cha DeploySSHUser: ws.cfg.DeploySSHUser, LogLevel: ws.cfg.LogLevel, Logger: &ws.logger, + AppliedConfig: ws.cfg.AppliedConfig, + UpdateStatus: updater.Snapshot, + ModuleStatus: runtimeStatus.moduleStatuses, } - agent, err := hostagent.New(hostCfg) if err != nil { ws.logger.Error().Err(err).Msg("Failed to create host agent") @@ -102,6 +112,8 @@ func (ws *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, cha changes <- svc.Status{State: svc.Stopped} return true, 1 } + remoteConfigAppliers = append(remoteConfigAppliers, agent) + runtimeStatus.setState("host", moduleStateRunning, nil) g.Go(func() error { ws.logger.Info().Msg("Host agent module started") @@ -109,7 +121,11 @@ func (ws *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, cha }) } - // Start Docker / Podman module (if enabled) + // Start Docker / Podman module (if enabled). Match the foreground runtime's + // retry semantics so a temporarily unavailable Docker Desktop pipe does not + // terminate the Windows service. + var dockerAgent RunnableCloser + defer func() { cleanupDockerAgent(dockerAgent, &ws.logger) }() if ws.cfg.EnableDocker { dockerCfg := dockeragent.Config{ PulseURL: ws.cfg.PulseURL, @@ -120,6 +136,8 @@ func (ws *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, cha AgentType: "unified", AgentVersion: Version, InsecureSkipVerify: ws.cfg.InsecureSkipVerify, + CACertPath: ws.cfg.CACertPath, + ServerFingerprint: ws.cfg.ServerFingerprint, DisableAutoUpdate: true, LogLevel: ws.cfg.LogLevel, Logger: &ws.logger, @@ -132,21 +150,83 @@ func (ws *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, cha agent, err := dockeragent.New(dockerCfg) if err != nil { - ws.logger.Error().Err(err).Msg("Failed to create Docker / Podman module") - if ws.eventLog != nil { - ws.eventLog.Error(1, fmt.Sprintf("Failed to create Docker / Podman module: %v", err)) - } - changes <- svc.Status{State: svc.Stopped} - return true, 1 + runtimeStatus.setState("docker", moduleStateRetrying, err) + ws.logger.Warn().Err(err).Msg("Docker / Podman module unavailable, retrying") + g.Go(func() error { + retried := initDockerWithRetry(ctx, dockerCfg, &ws.logger) + if retried == nil { + return nil + } + dockerAgent = retried + runtimeStatus.setState("docker", moduleStateRunning, nil) + return retried.Run(ctx) + }) + } else { + dockerAgent = agent + runtimeStatus.setState("docker", moduleStateRunning, nil) + g.Go(func() error { + ws.logger.Info().Msg("Docker / Podman module started") + return agent.Run(ctx) + }) } - - g.Go(func() error { - ws.logger.Info().Msg("Docker / Podman module started") - return agent.Run(ctx) - }) } - ready.Store(true) + if ws.cfg.EnableKubernetes { + kubeCfg := kubernetesagent.Config{ + PulseURL: ws.cfg.PulseURL, + APIToken: ws.cfg.APIToken, + Interval: ws.cfg.Interval, + AgentID: ws.cfg.AgentID, + AgentType: "unified", + AgentVersion: Version, + InsecureSkipVerify: ws.cfg.InsecureSkipVerify, + CACertPath: ws.cfg.CACertPath, + ServerFingerprint: ws.cfg.ServerFingerprint, + LogLevel: ws.cfg.LogLevel, + Logger: &ws.logger, + KubeconfigPath: ws.cfg.KubeconfigPath, + KubeContext: ws.cfg.KubeContext, + IncludeNamespaces: ws.cfg.KubeIncludeNamespaces, + ExcludeNamespaces: ws.cfg.KubeExcludeNamespaces, + IncludeAllPods: ws.cfg.KubeIncludeAllPods, + IncludeAllDeployments: ws.cfg.KubeIncludeAllDeployments, + MaxPods: ws.cfg.KubeMaxPods, + } + agent, err := kubernetesagent.New(kubeCfg) + if err != nil { + runtimeStatus.setState("kubernetes", moduleStateRetrying, err) + ws.logger.Warn().Err(err).Msg("Kubernetes module unavailable, retrying") + g.Go(func() error { + retried := initKubernetesWithRetry(ctx, kubeCfg, &ws.logger) + if retried == nil { + return nil + } + runtimeStatus.setState("kubernetes", moduleStateRunning, nil) + return retried.Run(ctx) + }) + } else { + runtimeStatus.setState("kubernetes", moduleStateRunning, nil) + g.Go(func() error { return agent.Run(ctx) }) + } + } + + if ws.cfg.PulseURL != "" && ws.cfg.APIToken != "" && ws.cfg.AgentID != "" && len(remoteConfigAppliers) > 0 { + client := remoteconfig.New(remoteconfig.Config{ + PulseURL: ws.cfg.PulseURL, + APIToken: ws.cfg.APIToken, + AgentID: ws.cfg.AgentID, + Hostname: ws.cfg.HostnameOverride, + InsecureSkipVerify: ws.cfg.InsecureSkipVerify, + CACertPath: ws.cfg.CACertPath, + ServerFingerprint: ws.cfg.ServerFingerprint, + Logger: ws.logger, + }) + defer client.Close() + g.Go(func() error { + runRemoteConfigLoop(ctx, client, remoteConfigAppliers, &ws.logger) + return nil + }) + } changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} ws.logger.Info(). diff --git a/deploy/provider-msp/Dockerfile.control-plane b/deploy/provider-msp/Dockerfile.control-plane index 0f3b32faf5..eabcf2d726 100644 --- a/deploy/provider-msp/Dockerfile.control-plane +++ b/deploy/provider-msp/Dockerfile.control-plane @@ -12,7 +12,7 @@ COPY SECURITY.md TERMS.md /app/ RUN --mount=type=cache,id=pulse-control-plane-npm-cache,target=/root/.npm \ npm run build -FROM --platform=$BUILDPLATFORM golang:1.25.11-alpine@sha256:8d95af53d0d58e1759ddb4028285d9b1239067e4fbf4f544618cad0f60fbc354 AS builder +FROM --platform=$BUILDPLATFORM golang:1.26.5-alpine@sha256:0178a641fbb4858c5f1b48e34bdaabe0350a330a1b1149aabd498d0699ff5fb2 AS builder ARG VERSION=dev ARG BUILD_TIME=unknown ARG GIT_COMMIT=unknown diff --git a/docs/release-control/v6/internal/UNIFIED_AGENT_PLATFORM_SUPPORT.md b/docs/release-control/v6/internal/UNIFIED_AGENT_PLATFORM_SUPPORT.md new file mode 100644 index 0000000000..1c4c0c4556 --- /dev/null +++ b/docs/release-control/v6/internal/UNIFIED_AGENT_PLATFORM_SUPPORT.md @@ -0,0 +1,60 @@ +# Unified Agent Platform Support Evidence + +This matrix separates a binary that can be cross-compiled from a lifecycle +that has been exercised on the operating system which will run it. A release +must not use cross-compilation alone as proof of native support. + +## Evidence levels + +- `native-ci`: tests and the built binary execute on a native GitHub-hosted OS + and architecture runner. +- `native-lab`: install, service start, report, update, restart/reboot, and + uninstall have been exercised on a maintained native lab target. +- `cross-build`: the release build produces the target and validates its file + format, but no native runtime claim follows from that evidence. +- `installer-contract`: the installer parser, state recovery, service + rendering, and teardown contracts have executable tests without a native + installed lifecycle. + +## Current matrix + +| Runtime target | Release artifact | Automated evidence | Remaining release-grade evidence | +| --- | --- | --- | --- | +| Linux amd64 | yes | native-ci gate | scheduled installed-service reboot/update rehearsal | +| Linux arm64 | yes | native-ci gate | scheduled installed-service reboot/update rehearsal | +| Linux armv7 | yes | cross-build | native lab lifecycle | +| Linux armv6 | yes | cross-build | native lab lifecycle | +| Linux 386 | yes | cross-build | native lab lifecycle | +| macOS arm64 | yes | native-ci gate; prior v5-to-v6 native-lab upgrade record | signed and notarized release candidate; recurring installed-service lifecycle | +| macOS amd64 | yes | native-ci gate | signed and notarized release candidate; recurring installed-service lifecycle | +| Windows amd64 | yes | native-ci gate | Authenticode-signed release candidate; recurring installed-service lifecycle | +| Windows arm64 | yes | cross-build | native lab lifecycle and Authenticode signing | +| Windows 386 | yes | cross-build | native lab lifecycle and Authenticode signing | +| FreeBSD amd64 | yes | cross-build with fail-closed ELF validation; installer-contract | native rc.d install/update/reboot/uninstall lifecycle | +| FreeBSD arm64 | yes | cross-build with fail-closed ELF validation; installer-contract | native rc.d install/update/reboot/uninstall lifecycle | + +`native-ci gate` means `.github/workflows/unified-agent-native.yml` now requires +the proof on relevant agent changes. The first successful workflow run is the +evidence event; the presence of the workflow file alone is not a green run. + +Appliance families such as Unraid, Synology, QNAP, TrueNAS, pfSense, and +OPNsense additionally require their own persistence and service-manager lab +proof. Reusing a Linux or FreeBSD binary does not establish appliance lifecycle +support. + +## Release trust boundary + +All release artifacts continue to require Pulse checksum, SSHSIG, and Ed25519 +verification. Those controls protect download integrity and the self-updater, +but they do not replace platform-native identity: + +- macOS distribution still requires Developer ID signing and Apple + notarization before the normal-user experience can be called release-grade. +- Windows distribution still requires Authenticode signing before the + normal-user experience can be called release-grade. + +Immutable candidate assembly now has native macOS and Windows signing jobs, +and normal release promotion requires them. The remaining external evidence is +a successful run with the repository's Developer ID/notary and Authenticode +credentials configured; missing credentials fail the release instead of +silently publishing unsigned desktop binaries. diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 9a4991e185..02f00e74a7 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -4122,3 +4122,43 @@ that waits for the persistent data volume before launching the stored wrapper, and `internal/agentupdate/update.go` keeps the persisted QNAP binary copy in sync on self-update so reboot does not roll the runtime back to an older binary. + +Unified Agent lifecycle truth is agent-authored. The host report contract now +carries the non-secret applied managed-config fingerprint, self-updater state +and timestamps, last successful source version, and per-module initialization +state. `/api/connections` may compare the reported applied fingerprint with the +canonical desired fingerprint and may expose updater or module failures, but it +must not synthesize those facts from server version comparison or from the +mere presence of a process. Successful update evidence observed on the first +post-restart report remains retained across later reports so a one-shot update +marker cannot disappear before an operator sees it. + +The runtime readiness boundary is module-aware. `/healthz` remains process +liveness, while `/readyz`, `/status`, and the module readiness Prometheus +gauges identify enabled modules that are starting, retrying, or running. An +enabled Docker or Kubernetes module that is still retrying initialization may +not be laundered into an overall ready state. + +Installer-owned connection continuity includes certificate pinning. Unix and +Windows installers must validate a supplied SHA-256 leaf-certificate +fingerprint, persist it in `connection.env`, recover it during later lifecycle +operations, and pass it to the long-lived service as +`--server-fingerprint`. A certificate pin is distinct from blanket insecure +TLS mode even when installer download tooling must temporarily bypass chain +validation after the explicit pin check. + +Native support evidence is governed by +`docs/release-control/v6/internal/UNIFIED_AGENT_PLATFORM_SUPPORT.md` and +`.github/workflows/unified-agent-native.yml`. Cross-compilation and archive +presence are build evidence only; platform support claims must name native CI, +native installed-lifecycle, appliance-lab, and platform code-signing evidence +separately. + +Normal release promotion requires platform-native identity for desktop agent +binaries. macOS agents must be Developer ID signed, submitted successfully to +Apple notarization, and accepted by `spctl`; Windows agents must carry a +verified Authenticode signature. Those native bytes must replace cross-built +desktop binaries before release packaging, Pulse checksum/signature creation, +SBOM generation, and immutable candidate manifest creation. Missing signing +credentials or failed native verification is a release failure, not a warning +or an unsigned fallback. diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 9d98c75a7c..20d36140c9 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -3168,6 +3168,16 @@ a new API state machine, queue contract, or verification-accounting field. ## Current State +Unified Agent connections now carry agent-authored lifecycle evidence through +the shared host model and `/api/connections` contract. The payload may include +the applied managed-config fingerprint, self-update state and timestamps, and +per-module enabled/running/error state. Fleet drift, update, and adapter-health +projections must derive from those reported facts when present while preserving +the existing unknown-compatible response for older agents that do not yet emit +them. Frontend API types and the settings connection ledger must stay in lockstep +with this backend shape; no UI-local reconstruction may infer a successful +update or module startup from the binary version alone. + Browser authentication cookies now consume one request-derived policy in `internal/api/auth.go`; session, CSRF, organization, magic-link, SAML, and cloud handoff flows must not materialize independent `Secure` or `SameSite` policy. diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 2264ee46bf..0e50a32a88 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -334,6 +334,9 @@ TLS floor in the dynamic config. local packet validation, manifest creation, and artifact upload; the observed release path requires a 60-minute ceiling even though the build itself is expected to finish much earlier. + Tarball entry validation must extract the requested files once per archive; + it must not decompress a multi-gigabyte release archive again for every + required entry. A manually dispatched release rehearsal must activate the same signed candidate build whenever its required `version` input is non-empty. Scheduled watchdog rehearsals omit that input and must skip candidate @@ -1075,7 +1078,7 @@ The governed v6 release Go patch level is part of that same boundary: `go.mod`, `scripts/.go-version`, `scripts/install-go-toolchain.sh`, `scripts/build-release.sh`, the Go builder stages in `Dockerfile` and `deploy/provider-msp/Dockerfile.control-plane`, and the Pro release workflows -must stay aligned on the same patched `1.25.x` floor before a release can be +must stay aligned on the same patched `1.26.x` floor before a release can be treated as shippable. When `govulncheck` reports called standard-library vulnerabilities in the current patch level, the canonical fix is to advance the governed release toolchain and immutable Go builder digest together, not to diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index 5ab14d2128..e106a51fb8 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -459,6 +459,16 @@ resource health. ## Current State +Unified Agent host reports now make module readiness and updater/config +lifecycle evidence monitoring-owned observed state. Monitoring preserves the +last successful one-shot update transition across subsequent reports, forwards +the applied config fingerprint without config values, and records Host, +Docker/Podman, and Kubernetes module failures so API consumers can distinguish +an active process from an initialized monitoring source. The Kubernetes module +uses the shared agent TLS constructor for custom CA and leaf-fingerprint trust; +its Pulse transport must not regress to a Kubernetes-local insecure-only TLS +configuration. + HTTP availability probes consume the shared explicitly unverified, parseable-peer-certificate capture boundary used by connection discovery, so support for operator self-signed diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index c514260b68..e2440d07e5 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -416,6 +416,14 @@ the `white_label` branding entitlement. ## Current State +Unified Agent Pulse transports share one fail-closed TLS policy across host, +Docker/Podman, Kubernetes, remote configuration, commands, and self-update. +Custom CA bundles and SHA-256 leaf-certificate pins are runtime trust inputs, +not monitoring data, and malformed pins must fail during client construction +instead of silently degrading to system roots or blanket skip-verification. +Installer persistence may carry the non-secret pin into service arguments, but +must not copy API tokens or certificate material into reports or diagnostics. + The browser-auth boundary now owns one request-derived cookie policy for every session, CSRF, organization, SAML, magic-link, and cloud-handoff cookie. Secure requests use Secure cookies and host-prefixed session names; explicitly diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 66c40848eb..c34ab4a98f 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -1615,6 +1615,14 @@ must not treat starter ## Current State +Unified Agent lifecycle fields added to the shared host and connections API are +adjacent monitoring/API state only. Applied config fingerprints, updater +status, and Host, Docker/Podman, or Kubernetes module readiness do not become +storage health, protection state, recovery points, backup verification, or +restore authorization. Storage and recovery consumers may use the canonical +host identity carried by those payloads, but must not derive recovery semantics +from agent process readiness or update success. + Authentication-cookie, SSO configured-file, deployment concurrency, and cloud handoff redirect hardening in shared `internal/api/` routes is adjacent API/security work only. It creates no storage-health, recovery-point, backup, diff --git a/frontend-modern/package-lock.json b/frontend-modern/package-lock.json index 3a84342c49..6205c18d63 100644 --- a/frontend-modern/package-lock.json +++ b/frontend-modern/package-lock.json @@ -46,9 +46,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", "dev": true, "license": "MIT" }, @@ -256,9 +256,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -326,13 +326,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -342,9 +342,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "dev": true, "license": "MIT", "engines": { @@ -1007,24 +1007,31 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1033,9 +1040,9 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1072,20 +1079,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "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.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1095,10 +1102,17 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1130,9 +1144,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1143,9 +1157,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -1180,29 +1194,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1262,14 +1290,39 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -1279,9 +1332,9 @@ } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "license": "MIT", "engines": { @@ -1339,9 +1392,9 @@ } }, "node_modules/@jscpd/badge-reporter": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@jscpd/badge-reporter/-/badge-reporter-4.0.4.tgz", - "integrity": "sha512-I9b4MmLXPM2vo0SxSUWnNGKcA4PjQlD3GzXvFK60z43cN/EIdLbOq3FVwCL+dg2obUqGXKIzAm7EsDFTg0D+mQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@jscpd/badge-reporter/-/badge-reporter-4.2.5.tgz", + "integrity": "sha512-ktXrjPeRaRyUDktxTroSA2/w5sshXpQplWkUuq/e6XqEpKBSbGEnwZLIaegSijOrMwIcCXPQ9k4feXIz5eVJNA==", "dev": true, "license": "MIT", "dependencies": { @@ -1351,9 +1404,9 @@ } }, "node_modules/@jscpd/core": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@jscpd/core/-/core-4.0.4.tgz", - "integrity": "sha512-QGMT3iXEX1fI6lgjPH+x8eyJwhwr2KkpSF5uBpjC0Z5Xloj0yFTFLtwJT+RhxP/Ob4WYrtx2jvpKB269oIwgMQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@jscpd/core/-/core-4.2.5.tgz", + "integrity": "sha512-Esf2deHxaoNEjePwf2jqP6Urzj+BAOsJVPFLbnnSsV+q7rLNMcn0UEEoKBXIOOt4qMkrkhl9DfwpMyPPOr6GkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1361,14 +1414,14 @@ } }, "node_modules/@jscpd/finder": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@jscpd/finder/-/finder-4.0.4.tgz", - "integrity": "sha512-qVUWY7Nzuvfd5OIk+n7/5CM98LmFroLqblRXAI2gDABwZrc7qS+WH2SNr0qoUq0f4OqwM+piiwKvwL/VDNn/Cg==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@jscpd/finder/-/finder-4.2.5.tgz", + "integrity": "sha512-Rw0dtwp/EeLANbujOubuQeJIuXXXkAlT+f5geZhwkB9TxEYP0hqNrdOJUK/TDBKQjRGrOizEtdNy+S4UlbdzOQ==", "dev": true, "license": "MIT", "dependencies": { - "@jscpd/core": "4.0.4", - "@jscpd/tokenizer": "4.0.4", + "@jscpd/core": "4.2.5", + "@jscpd/tokenizer": "4.2.5", "blamer": "^1.0.6", "bytes": "^3.1.2", "cli-table3": "^0.6.5", @@ -1376,30 +1429,29 @@ "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "markdown-table": "^2.0.0", - "pug": "^3.0.3" + "pug": "^3.0.4" } }, "node_modules/@jscpd/html-reporter": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@jscpd/html-reporter/-/html-reporter-4.0.4.tgz", - "integrity": "sha512-YiepyeYkeH74Kx59PJRdUdonznct0wHPFkf6FLQN+mCBoy6leAWCcOfHtcexnp+UsBFDlItG5nRdKrDSxSH+Kg==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@jscpd/html-reporter/-/html-reporter-4.2.5.tgz", + "integrity": "sha512-zMMIKbvi43dMgeNeHXlHQy1ovf+KJrzNlUubaBvCAVatqP23ksW8d3fmsevIQG9mMMTH0D1xOz+SxUn1FREOPg==", "dev": true, "license": "MIT", "dependencies": { "colors": "1.4.0", "fs-extra": "^11.2.0", - "pug": "^3.0.3" + "pug": "^3.0.4" } }, "node_modules/@jscpd/tokenizer": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@jscpd/tokenizer/-/tokenizer-4.0.4.tgz", - "integrity": "sha512-xxYYY/qaLah/FlwogEbGIxx9CjDO+G9E6qawcy26WwrflzJb6wsnhjwdneN6Wb0RNCDsqvzY+bzG453jsin4UQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@jscpd/tokenizer/-/tokenizer-4.2.5.tgz", + "integrity": "sha512-UM8Wx/jwahmflqQExlcKMQTYOAy58N/fn7Pv6NYrkD3EZm/FTk7gW97wkXy5aDE1Ts9oBUpT9tLY2rz7ogCHAQ==", "dev": true, "license": "MIT", "dependencies": { - "@jscpd/core": "4.0.4", - "reprism": "^0.0.11", + "@jscpd/core": "4.2.5", "spark-md5": "^3.0.2" } }, @@ -1453,9 +1505,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", - "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", "cpu": [ "arm" ], @@ -1467,9 +1519,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", - "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", "cpu": [ "arm64" ], @@ -1481,9 +1533,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", - "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", "cpu": [ "arm64" ], @@ -1495,9 +1547,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", - "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -1509,9 +1561,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", - "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", "cpu": [ "arm64" ], @@ -1523,9 +1575,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", - "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -1537,13 +1589,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", - "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1551,13 +1606,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", - "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1565,13 +1623,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", - "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1579,13 +1640,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", - "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1593,13 +1657,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", - "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1607,13 +1674,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", - "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1621,13 +1691,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", - "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1635,13 +1708,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", - "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1649,13 +1725,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", - "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1663,13 +1742,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", - "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1677,13 +1759,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", - "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1691,13 +1776,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", - "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1705,13 +1793,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", - "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1719,9 +1810,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", - "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", "cpu": [ "x64" ], @@ -1733,9 +1824,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", - "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -1747,9 +1838,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", - "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ "arm64" ], @@ -1761,9 +1852,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", - "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ "ia32" ], @@ -1775,9 +1866,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", - "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -1789,9 +1880,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", - "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", "cpu": [ "x64" ], @@ -1834,16 +1925,16 @@ } }, "node_modules/@tailwindcss/typography": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", - "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz", + "integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==", "dev": true, "license": "MIT", "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" } }, "node_modules/@testing-library/dom": { @@ -1964,9 +2055,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1978,9 +2069,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz", - "integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { @@ -2012,20 +2103,20 @@ "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", - "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", + "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/type-utils": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/type-utils": "8.63.0", + "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2035,22 +2126,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.63.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", - "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", + "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", "debug": "^4.4.3" }, "engines": { @@ -2061,19 +2152,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", - "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", + "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.54.0", - "@typescript-eslint/types": "^8.54.0", + "@typescript-eslint/tsconfig-utils": "^8.63.0", + "@typescript-eslint/types": "^8.63.0", "debug": "^4.4.3" }, "engines": { @@ -2084,18 +2175,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", - "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", + "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0" + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2106,9 +2197,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", - "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", + "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", "dev": true, "license": "MIT", "engines": { @@ -2119,21 +2210,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", - "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", + "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2143,14 +2234,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", - "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", + "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", "dev": true, "license": "MIT", "engines": { @@ -2162,21 +2253,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", - "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", + "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.54.0", - "@typescript-eslint/tsconfig-utils": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/project-service": "8.63.0", + "@typescript-eslint/tsconfig-utils": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/visitor-keys": "8.63.0", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2186,20 +2277,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", - "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", + "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0" + "@typescript-eslint/scope-manager": "8.63.0", + "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2209,19 +2300,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", - "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", + "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.63.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2232,22 +2323,22 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz", - "integrity": "sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz", + "integrity": "sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==", "dev": true, "license": "MIT", "dependencies": { @@ -2269,8 +2360,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.6", - "vitest": "3.2.6" + "@vitest/browser": "3.2.7", + "vitest": "3.2.7" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2279,15 +2370,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", - "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -2296,13 +2387,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", - "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.6", + "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -2323,9 +2414,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", - "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2336,13 +2427,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", - "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.6", + "@vitest/utils": "3.2.7", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -2351,13 +2442,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", - "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", + "@vitest/pretty-format": "3.2.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -2366,9 +2457,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", - "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2379,13 +2470,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", - "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.6", + "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, @@ -2394,9 +2485,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", "bin": { @@ -2427,9 +2518,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2537,21 +2628,21 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.9.tgz", - "integrity": "sha512-dSC6tJeOJxbZrPzPbv5mMd6CMiQ1ugaVXXPRad2fXUSsy1kstFn9XQWemV9VW7Y7kpxgQ/4WMoZfwdH8XSU48w==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", - "js-tokens": "^9.0.1" + "js-tokens": "^10.0.0" } }, "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -2563,9 +2654,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.22", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", - "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", "dev": true, "funding": [ { @@ -2583,10 +2674,9 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.27.0", - "caniuse-lite": "^1.0.30001754", + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", "fraction.js": "^5.3.4", - "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, @@ -2601,9 +2691,9 @@ } }, "node_modules/babel-plugin-jsx-dom-expressions": { - "version": "0.40.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.3.tgz", - "integrity": "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==", + "version": "0.40.7", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.7.tgz", + "integrity": "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2631,17 +2721,17 @@ } }, "node_modules/babel-preset-solid": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.10.tgz", - "integrity": "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==", + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.12.tgz", + "integrity": "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jsx-dom-expressions": "^0.40.3" + "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", - "solid-js": "^1.9.10" + "solid-js": "^1.9.12" }, "peerDependenciesMeta": { "solid-js": { @@ -2663,27 +2753,33 @@ } }, "node_modules/badgen": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/badgen/-/badgen-3.2.3.tgz", - "integrity": "sha512-svDuwkc63E/z0ky3drpUppB83s/nlgDciH9m+STwwQoWyq7yCgew1qEfJ+9axkKdNq7MskByptWUN9j1PGMwFA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/badgen/-/badgen-3.3.2.tgz", + "integrity": "sha512-fbQwK9norfdzbdsoPwbLIAmgBXDGEme3jeIyqPAH7o6vp9lmuLHS7uXULvOiQ6XnMLkYNG4gDjILf74hgtTAug==", "dev": true, "license": "MIT" }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.7", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz", - "integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==", + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/binary-extensions": { @@ -2714,9 +2810,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -2726,16 +2822,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/brace-expansion/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -2750,9 +2836,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", "dev": true, "funding": [ { @@ -2770,11 +2856,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -2864,9 +2950,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001760", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", - "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", "dev": true, "funding": [ { @@ -2992,28 +3078,6 @@ "@colors/colors": "1.5.0" } }, - "node_modules/cli-table3/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-table3/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -3025,26 +3089,6 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -3101,13 +3145,13 @@ } }, "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=22.12.0" } }, "node_modules/concat-map": { @@ -3348,17 +3392,16 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/end-of-stream": { @@ -3412,9 +3455,9 @@ "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -3506,25 +3549,25 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@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.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -3543,7 +3586,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -3633,10 +3676,17 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -3668,9 +3718,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", - "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3712,9 +3762,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3798,17 +3848,10 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3867,9 +3910,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -3957,6 +4000,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -3989,9 +4045,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", "dev": true, "license": "MIT", "dependencies": { @@ -4003,16 +4059,6 @@ "node": ">=14.14" } }, - "node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4112,14 +4158,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gitignore-to-glob": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/gitignore-to-glob/-/gitignore-to-glob-0.3.0.tgz", - "integrity": "sha512-mk74BdnK7lIwDHnotHddx1wsjMOFIThpLY3cPNniJ/2fA/tlLzHnFxIdR+4sLOu5KGgQJdij4kjJ2RoUNnCNMA==", + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.4 <5 || >=6.9" + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob-parent": { @@ -4135,10 +4193,43 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", - "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", "dev": true, "license": "MIT", "engines": { @@ -4388,13 +4479,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -4666,47 +4757,36 @@ } }, "node_modules/jscpd": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-4.0.8.tgz", - "integrity": "sha512-d2VNT/2Hv4dxT2/59He8Lyda4DYOxPRyRG9zBaOpTZAqJCVf2xLrBlZkT8Va6Lo9u3X2qz8Bpq4HrDi4JsrQhA==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-4.2.5.tgz", + "integrity": "sha512-KDpApYw1ChGelfHb7MwYTEx694OnW52pv3McAasidUV4ILcGDQMiVJzB+vI8ox+ZPVfOSvdXQCk8uRa9B0LXnw==", "dev": true, "license": "MIT", "dependencies": { - "@jscpd/badge-reporter": "4.0.4", - "@jscpd/core": "4.0.4", - "@jscpd/finder": "4.0.4", - "@jscpd/html-reporter": "4.0.4", - "@jscpd/tokenizer": "4.0.4", + "@jscpd/badge-reporter": "4.2.5", + "@jscpd/core": "4.2.5", + "@jscpd/finder": "4.2.5", + "@jscpd/html-reporter": "4.2.5", + "@jscpd/tokenizer": "4.2.5", "colors": "^1.4.0", - "commander": "^5.0.0", + "commander": "^15.0.0", "fs-extra": "^11.2.0", - "gitignore-to-glob": "^0.3.0", - "jscpd-sarif-reporter": "4.0.6" + "jscpd-sarif-reporter": "4.2.5" }, "bin": { "jscpd": "bin/jscpd" } }, "node_modules/jscpd-sarif-reporter": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/jscpd-sarif-reporter/-/jscpd-sarif-reporter-4.0.6.tgz", - "integrity": "sha512-b9Sm3IPZ3+m8Lwa4gZa+4/LhDhlc/ZLEsLXKSOy1DANQ6kx0ueqZT+fUHWEdQ6m0o3+RIVIa7DmvLSojQD05ng==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/jscpd-sarif-reporter/-/jscpd-sarif-reporter-4.2.5.tgz", + "integrity": "sha512-O8LcM9grAS5yO5x1Q0yegYaYcUX//IEBEyvzGFSYCeo1YzHbMnAI6EK7oTrwD+7Csjvfg9m8B8G7OOxzcSlr9w==", "dev": true, "license": "MIT", "dependencies": { "colors": "^1.4.0", "fs-extra": "^11.2.0", - "node-sarif-builder": "^3.4.0" - } - }, - "node_modules/jscpd/node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" + "node-sarif-builder": "^4.1.0" } }, "node_modules/jsdom": { @@ -4798,9 +4878,9 @@ } }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4810,16 +4890,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonfile/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/jstransformer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", @@ -4998,9 +5068,9 @@ } }, "node_modules/marked": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", - "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.6.tgz", + "integrity": "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -5110,27 +5180,27 @@ } }, "node_modules/minimatch": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz", - "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -5155,9 +5225,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -5181,16 +5251,19 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/node-sarif-builder": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", - "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-4.1.0.tgz", + "integrity": "sha512-IWqZF6u0EI/07HTBm+zZ+MgXgWl09dnSJRGaDCPBSlOqilDcx6pj3Mpb3HvPN8V2Gr+ISw7ZrMsL7STWs1F++w==", "dev": true, "license": "MIT", "dependencies": { @@ -5211,16 +5284,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -5235,9 +5298,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", "dev": true, "license": "MIT" }, @@ -5489,9 +5552,9 @@ } }, "node_modules/postcss": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", - "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -5509,7 +5572,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5631,9 +5694,9 @@ } }, "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5676,9 +5739,9 @@ } }, "node_modules/prettier": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", - "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", "dev": true, "license": "MIT", "bin": { @@ -5743,13 +5806,13 @@ } }, "node_modules/pug": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", - "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.4.tgz", + "integrity": "sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==", "dev": true, "license": "MIT", "dependencies": { - "pug-code-gen": "^3.0.3", + "pug-code-gen": "^3.0.4", "pug-filters": "^4.0.0", "pug-lexer": "^5.0.1", "pug-linker": "^4.0.0", @@ -5772,9 +5835,9 @@ } }, "node_modules/pug-code-gen": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", - "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.4.tgz", + "integrity": "sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==", "dev": true, "license": "MIT", "dependencies": { @@ -5879,9 +5942,9 @@ "license": "MIT" }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "dependencies": { @@ -5998,13 +6061,6 @@ "node": ">=0.10" } }, - "node_modules/reprism": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/reprism/-/reprism-0.0.11.tgz", - "integrity": "sha512-VsxDR5QxZo08M/3nRypNlScw5r3rKeSOPdU/QhDmu3Ai3BJxHn/qgfXGWQp/tAxUtzwYNo9W6997JZR0tPLZsA==", - "dev": true, - "license": "MIT" - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6028,12 +6084,13 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -6070,13 +6127,13 @@ } }, "node_modules/rollup": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", - "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -6086,31 +6143,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.1", - "@rollup/rollup-android-arm64": "4.60.1", - "@rollup/rollup-darwin-arm64": "4.60.1", - "@rollup/rollup-darwin-x64": "4.60.1", - "@rollup/rollup-freebsd-arm64": "4.60.1", - "@rollup/rollup-freebsd-x64": "4.60.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", - "@rollup/rollup-linux-arm-musleabihf": "4.60.1", - "@rollup/rollup-linux-arm64-gnu": "4.60.1", - "@rollup/rollup-linux-arm64-musl": "4.60.1", - "@rollup/rollup-linux-loong64-gnu": "4.60.1", - "@rollup/rollup-linux-loong64-musl": "4.60.1", - "@rollup/rollup-linux-ppc64-gnu": "4.60.1", - "@rollup/rollup-linux-ppc64-musl": "4.60.1", - "@rollup/rollup-linux-riscv64-gnu": "4.60.1", - "@rollup/rollup-linux-riscv64-musl": "4.60.1", - "@rollup/rollup-linux-s390x-gnu": "4.60.1", - "@rollup/rollup-linux-x64-gnu": "4.60.1", - "@rollup/rollup-linux-x64-musl": "4.60.1", - "@rollup/rollup-openbsd-x64": "4.60.1", - "@rollup/rollup-openharmony-arm64": "4.60.1", - "@rollup/rollup-win32-arm64-msvc": "4.60.1", - "@rollup/rollup-win32-ia32-msvc": "4.60.1", - "@rollup/rollup-win32-x64-gnu": "4.60.1", - "@rollup/rollup-win32-x64-msvc": "4.60.1", + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" } }, @@ -6166,9 +6223,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -6179,18 +6236,18 @@ } }, "node_modules/seroval": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.2.tgz", - "integrity": "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.5.tgz", + "integrity": "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw==", "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/seroval-plugins": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.4.2.tgz", - "integrity": "sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.5.tgz", + "integrity": "sha512-+BDhqYM6CEn3x09v44dpa9p6974FuUB2dxk+Ctn04k0cO1Zt6QODTXfmEZK0eBaTe/fJBvP4NMGuNJ+R8T+QMg==", "license": "MIT", "engines": { "node": ">=10" @@ -6236,27 +6293,21 @@ "license": "ISC" }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "license": "ISC" }, "node_modules/solid-js": { - "version": "1.9.10", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz", - "integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==", + "version": "1.9.14", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.14.tgz", + "integrity": "sha512-sAEXC0Kk0S1EDg+8ysEWJDbYhA3RRoEjwuySUGlKIemeo0I5YZfOyumNjNs9Sv3y2nmhD+0rW66ag2HsMuQiGQ==", "license": "MIT", "dependencies": { "csstype": "^3.1.0", - "seroval": "~1.3.0", - "seroval-plugins": "~1.3.0" + "seroval": "~1.5.4", + "seroval-plugins": "~1.5.4" } }, "node_modules/solid-refresh": { @@ -6306,21 +6357,17 @@ "license": "MIT" }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/string-width-cjs": { @@ -6339,42 +6386,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6490,6 +6501,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6562,9 +6583,9 @@ } }, "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6576,41 +6597,20 @@ } }, "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", - "minimatch": "^9.0.4" + "minimatch": "^10.2.2" }, "engines": { "node": ">=18" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6649,14 +6649,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -6684,9 +6684,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -6762,6 +6762,16 @@ "node": ">=6" } }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", @@ -6776,9 +6786,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -6823,16 +6833,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", - "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "version": "8.63.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", + "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.54.0", - "@typescript-eslint/parser": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0" + "@typescript-eslint/eslint-plugin": "8.63.0", + "@typescript-eslint/parser": "8.63.0", + "@typescript-eslint/typescript-estree": "8.63.0", + "@typescript-eslint/utils": "8.63.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6842,8 +6852,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/undici-types": { @@ -6854,19 +6864,19 @@ "license": "MIT" }, "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4.0.0" + "node": ">= 10.0.0" } }, "node_modules/update-browserslist-db": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", - "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -7021,9 +7031,9 @@ } }, "node_modules/vite-plugin-solid": { - "version": "2.11.10", - "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.10.tgz", - "integrity": "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==", + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.12.tgz", + "integrity": "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA==", "dev": true, "license": "MIT", "dependencies": { @@ -7037,7 +7047,7 @@ "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@testing-library/jest-dom": { @@ -7046,9 +7056,9 @@ } }, "node_modules/vite-plugin-sri-gen": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/vite-plugin-sri-gen/-/vite-plugin-sri-gen-1.3.2.tgz", - "integrity": "sha512-/tbM2168Aeiw9jV1+4mzno/ZDMT9aZ2qh+mi10DBlLc+IfuTqn3BFFI6fQS1s6xsfPzhzCRFSoPjP3NrsTvoDQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/vite-plugin-sri-gen/-/vite-plugin-sri-gen-1.7.1.tgz", + "integrity": "sha512-HIv+DtfQD+GJwmM1FhAKUVAybHFudB3oDDUB/hSJ0DpDK+UyT7MTCXs3bgGH7cZcbXnex+/62U2Dlcfe1zZgeA==", "dev": true, "license": "MIT", "dependencies": { @@ -7061,14 +7071,27 @@ "vite": ">=4.0.0" } }, - "node_modules/vite-plugin-sri-gen/node_modules/parse5": { + "node_modules/vite-plugin-sri-gen/node_modules/entities": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/vite-plugin-sri-gen/node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^6.0.0" + "entities": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -7093,9 +7116,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -7106,9 +7129,9 @@ } }, "node_modules/vitefu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", - "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", "dev": true, "license": "MIT", "workspaces": [ @@ -7117,7 +7140,7 @@ "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "vite": { @@ -7126,20 +7149,20 @@ } }, "node_modules/vitest": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", - "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.6", - "@vitest/mocker": "3.2.6", - "@vitest/pretty-format": "^3.2.6", - "@vitest/runner": "3.2.6", - "@vitest/snapshot": "3.2.6", - "@vitest/spy": "3.2.6", - "@vitest/utils": "3.2.6", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -7169,8 +7192,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.6", - "@vitest/ui": "3.2.6", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, @@ -7199,9 +7222,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -7248,6 +7271,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", "dependencies": { @@ -7383,28 +7407,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -7431,14 +7433,39 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -7541,12 +7568,6 @@ "node": ">=6" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/yargs/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -7599,20 +7620,6 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend-modern/src/api/connections.ts b/frontend-modern/src/api/connections.ts index 4fada28c49..b481306588 100644 --- a/frontend-modern/src/api/connections.ts +++ b/frontend-modern/src/api/connections.ts @@ -34,10 +34,14 @@ export type ConnectionFleetAdapterHealth = export type ConnectionFleetConfigRollout = 'configured' | 'paused' | 'reported' | 'unknown'; export type ConnectionFleetCredentialStatus = 'invalid' | 'paused' | 'unknown' | 'verified'; export type ConnectionFleetUpdateStatus = + | 'checking' | 'current' + | 'disabled' + | 'failed' | 'not-applicable' | 'unknown' - | 'update-available'; + | 'update-available' + | 'updating'; export type ConnectionFleetRemoteControl = 'disabled' | 'enabled' | 'not-applicable' | 'unknown'; export type ConnectionFleetConfigDriftStatus = | 'current' @@ -155,6 +159,25 @@ export interface ConnectionAgentIdentity { commandsEnabled?: boolean; } +export interface ConnectionAgentUpdateStatus { + state: 'idle' | 'checking' | 'update-available' | 'updating' | 'error' | 'disabled'; + autoUpdate: boolean; + updatedFrom?: string; + availableVersion?: string; + lastCheckedAt?: string; + lastAttemptAt?: string; + lastSuccessAt?: string; + lastError?: string; +} + +export interface ConnectionAgentModuleStatus { + name: string; + enabled: boolean; + state: 'disabled' | 'starting' | 'retrying' | 'running'; + lastError?: string; + updatedAt: string; +} + export interface Connection { id: string; type: ConnectionType; @@ -173,6 +196,8 @@ export interface Connection { agentVersion?: string; expectedAgentVersion?: string; agentUpdateAvailable?: boolean; + agentUpdate?: ConnectionAgentUpdateStatus; + agentModules?: ConnectionAgentModuleStatus[]; fleet?: ConnectionFleetGovernance; capabilities: ConnectionCapabilities; } diff --git a/frontend-modern/src/components/Settings/__tests__/connectionsTableModel.test.ts b/frontend-modern/src/components/Settings/__tests__/connectionsTableModel.test.ts index f0542c9e7b..a5f7e40550 100644 --- a/frontend-modern/src/components/Settings/__tests__/connectionsTableModel.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/connectionsTableModel.test.ts @@ -25,6 +25,89 @@ const connectionFixture = (overrides: Partial = {}): Connection => ( }); describe('visibleFleetGovernanceSignals', () => { + it('surfaces the failed module and its operator-facing reason', () => { + const signals = fleetGovernanceSignalsForConnection( + connectionFixture({ + agentModules: [ + { + name: 'docker', + enabled: true, + state: 'retrying', + lastError: 'cannot connect to /var/run/docker.sock', + updatedAt: '2026-07-09T12:00:00Z', + }, + ], + fleet: { + enrollmentState: 'enrolled', + livenessState: 'active', + versionDrift: 'current', + adapterHealth: 'degraded', + configRollout: 'reported', + credentialStatus: 'verified', + updateStatus: 'current', + remoteControl: 'disabled', + }, + }), + ); + + expect(visibleFleetGovernanceSignals(signals)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: 'module-health', + label: 'Docker module retrying', + detail: 'cannot connect to /var/run/docker.sock', + tone: 'warning', + }), + ]), + ); + }); + + it('keeps updater failures and disabled auto-update actionable', () => { + const failed = fleetGovernanceSignalsForConnection( + connectionFixture({ + agentUpdate: { + state: 'error', + autoUpdate: true, + lastError: 'signature verification failed', + }, + fleet: { + enrollmentState: 'enrolled', + livenessState: 'active', + versionDrift: 'behind', + adapterHealth: 'healthy', + configRollout: 'reported', + credentialStatus: 'verified', + updateStatus: 'failed', + remoteControl: 'disabled', + }, + }), + ); + const disabled = fleetGovernanceSignalsForConnection( + connectionFixture({ + fleet: { + enrollmentState: 'enrolled', + livenessState: 'active', + versionDrift: 'behind', + adapterHealth: 'healthy', + configRollout: 'reported', + credentialStatus: 'verified', + updateStatus: 'disabled', + remoteControl: 'disabled', + }, + }), + ); + + expect(failed.find((signal) => signal.key === 'updates')).toMatchObject({ + label: 'Agent update failed', + detail: 'signature verification failed', + tone: 'critical', + }); + expect(disabled.find((signal) => signal.key === 'updates')).toMatchObject({ + label: 'Auto-update off', + tone: 'warning', + }); + }); + it('hides passive agent config fingerprint handshakes while preserving raw fleet facts', () => { const rawSignals = fleetGovernanceSignalsForConnection( connectionFixture({ diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index fe1d4496ea..8b958a92ec 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -43,6 +43,7 @@ import infrastructureWorkspaceSource from '../InfrastructureWorkspace.tsx?raw'; import infrastructureInstallerSectionSource from '../InfrastructureInstallerSection.tsx?raw'; import infrastructureOperationsModelSource from '../infrastructureOperationsModel.tsx?raw'; import connectionsTableModelSource from '../connectionsTableModel.ts?raw'; +import connectionsApiSource from '@/api/connections.ts?raw'; import infrastructureSourceManagerSource from '../InfrastructureSourceManager.tsx?raw'; import infrastructureSourcePickerSource from '../InfrastructureSourcePicker.tsx?raw'; import discoverySettingsFormSource from '../DiscoverySettingsForm.tsx?raw'; @@ -103,6 +104,15 @@ const settingsRuntimeSources = import.meta.glob(['../*.tsx', '../ConnectionEdito }) as Record; describe('settings architecture guardrails', () => { + it('keeps Unified Agent lifecycle failures on the shared connections contract', () => { + expect(connectionsApiSource).toContain("| 'failed'"); + expect(connectionsApiSource).toContain('agentUpdate?: ConnectionAgentUpdateStatus;'); + expect(connectionsApiSource).toContain('agentModules?: ConnectionAgentModuleStatus[];'); + expect(connectionsTableModelSource).toContain("key: 'module-health'"); + expect(connectionsTableModelSource).toContain("label: 'Agent update failed'"); + expect(connectionsTableModelSource).toContain('update?.lastError'); + }); + it('keeps Settings on the canonical page shell boundary', () => { expect(settingsSource).toContain("import { SettingsDialogs } from './SettingsDialogs';"); expect(settingsSource).toContain("import { SettingsPageShell } from './SettingsPageShell';"); diff --git a/frontend-modern/src/components/Settings/connectionsTableModel.ts b/frontend-modern/src/components/Settings/connectionsTableModel.ts index 671c9728ab..2fdb38f5cd 100644 --- a/frontend-modern/src/components/Settings/connectionsTableModel.ts +++ b/frontend-modern/src/components/Settings/connectionsTableModel.ts @@ -239,6 +239,7 @@ export type FleetGovernanceSignalKey = | 'liveness' | 'version' | 'adapter' + | 'module-health' | 'config' | 'config-drift' | 'credentials' @@ -666,7 +667,10 @@ const credentialHealthSignal = (state: ConnectionFleetCredentialHealth): FleetGo } }; -const updateSignal = (state: ConnectionFleetUpdateStatus): FleetGovernanceSignal => { +const updateSignal = ( + state: ConnectionFleetUpdateStatus, + update?: Connection['agentUpdate'], +): FleetGovernanceSignal => { switch (state) { case 'update-available': return { @@ -675,6 +679,36 @@ const updateSignal = (state: ConnectionFleetUpdateStatus): FleetGovernanceSignal detail: 'A newer Pulse Agent binary is available for this system.', tone: 'warning', }; + case 'checking': + return { + key: 'updates', + label: 'Checking for updates', + detail: 'This agent is checking the Pulse server for an updated binary.', + tone: 'info', + }; + case 'updating': + return { + key: 'updates', + label: 'Agent updating', + detail: 'This agent is verifying and applying an updated binary.', + tone: 'info', + }; + case 'failed': + return { + key: 'updates', + label: 'Agent update failed', + detail: + update?.lastError || + 'The agent could not complete its latest update check or update attempt.', + tone: 'critical', + }; + case 'disabled': + return { + key: 'updates', + label: 'Auto-update off', + detail: 'Automatic Pulse Agent updates are disabled on this system.', + tone: 'warning', + }; case 'current': return { key: 'updates', @@ -699,6 +733,30 @@ const updateSignal = (state: ConnectionFleetUpdateStatus): FleetGovernanceSignal } }; +const moduleHealthSignal = (connection: Connection): FleetGovernanceSignal | undefined => { + const module = connection.agentModules?.find( + (candidate) => candidate.enabled && candidate.state !== 'running', + ); + if (!module) return undefined; + + const displayName = + module.name === 'docker' + ? 'Docker' + : module.name === 'kubernetes' + ? 'Kubernetes' + : module.name === 'host' + ? 'Host' + : module.name; + return { + key: 'module-health', + label: `${displayName} module ${module.state}`, + detail: + module.lastError || + `${displayName} monitoring is enabled but the module is not running yet.`, + tone: 'warning', + }; +}; + const commandPolicySignal = (state: ConnectionFleetCommandPolicy): FleetGovernanceSignal => { if (state.enforcement === 'drifted') { const desiredDisabledAppliedEnabled = @@ -825,11 +883,13 @@ export const fleetGovernanceSignalsForConnection = ( credentialHealthSignal(credentialHealthFromFleet(fleet)), ]; if (runsAgentFleet) { + const moduleSignal = moduleHealthSignal(connection); + if (moduleSignal) signals.push(moduleSignal); signals.push( configDriftSignal(configDriftFromFleet(fleet)), rolloutSignal(rolloutFromFleet(fleet)), versionSignal(fleet.versionDrift), - updateSignal(fleet.updateStatus), + updateSignal(fleet.updateStatus, connection.agentUpdate), ); } signals.push(adapterSignal(fleet.adapterHealth)); diff --git a/frontend-modern/src/types/api.ts b/frontend-modern/src/types/api.ts index d780f4ea1f..2c001dcea3 100644 --- a/frontend-modern/src/types/api.ts +++ b/frontend-modern/src/types/api.ts @@ -459,6 +459,9 @@ export interface Agent { intervalSeconds?: number; agentVersion?: string; commandsEnabled?: boolean; // Whether AI command execution is enabled on this agent + appliedConfig?: AgentConfigFingerprint; + agentUpdate?: AgentUpdateStatus; + agentModules?: AgentModuleStatus[]; tokenId?: string; tokenName?: string; tokenHint?: string; @@ -473,6 +476,30 @@ export interface Agent { linkedContainerId?: string; // ID of container this agent is running inside } +export interface AgentConfigFingerprint { + version: string; + hash: string; +} + +export interface AgentUpdateStatus { + state: 'idle' | 'checking' | 'update-available' | 'updating' | 'error' | 'disabled'; + autoUpdate: boolean; + updatedFrom?: string; + availableVersion?: string; + lastCheckedAt?: string; + lastAttemptAt?: string; + lastSuccessAt?: string; + lastError?: string; +} + +export interface AgentModuleStatus { + name: string; + enabled: boolean; + state: 'disabled' | 'starting' | 'retrying' | 'running'; + lastError?: string; + updatedAt: string; +} + export interface HostNetworkInterface { name: string; mac?: string; diff --git a/go.mod b/go.mod index 7e9d2826bd..0a0bf3dfd5 100644 --- a/go.mod +++ b/go.mod @@ -1,41 +1,42 @@ module github.com/rcourtman/pulse-go-rewrite -go 1.25.0 +go 1.26.0 -toolchain go1.25.11 +toolchain go1.26.5 require ( - github.com/IGLOU-EU/go-wildcard/v2 v2.1.0 + github.com/IGLOU-EU/go-wildcard/v2 v2.1.1 github.com/containerd/errdefs v1.0.0 - github.com/coreos/go-oidc/v3 v3.17.0 + github.com/coreos/go-oidc/v3 v3.20.0 github.com/crewjam/saml v0.5.1 - github.com/fsnotify/fsnotify v1.9.0 + github.com/fsnotify/fsnotify v1.10.1 github.com/go-pdf/fpdf v0.9.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 - github.com/gorilla/websocket v1.5.3 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/joho/godotenv v1.5.1 - github.com/moby/moby/api v1.54.0 - github.com/moby/moby/client v0.3.0 + github.com/moby/moby/api v1.55.0 + github.com/moby/moby/client v0.5.0 github.com/oklog/ulid/v2 v2.1.1 github.com/opencontainers/image-spec v1.1.1 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 - github.com/rs/zerolog v1.34.0 - github.com/shirou/gopsutil/v4 v4.25.11 - github.com/spf13/cobra v1.10.1 + github.com/rs/zerolog v1.35.1 + github.com/shirou/gopsutil/v4 v4.26.6 + github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/stripe/stripe-go/v82 v82.5.1 - golang.org/x/crypto v0.53.0 - golang.org/x/oauth2 v0.35.0 - golang.org/x/sync v0.21.0 - golang.org/x/sys v0.46.0 - golang.org/x/term v0.44.0 - k8s.io/api v0.32.0 - k8s.io/apimachinery v0.32.0 - k8s.io/client-go v0.32.0 - modernc.org/sqlite v1.40.1 + golang.org/x/crypto v0.54.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.22.0 + golang.org/x/sys v0.47.0 + golang.org/x/term v0.45.0 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.36.2 + k8s.io/apimachinery v0.36.2 + k8s.io/client-go v0.36.2 + modernc.org/sqlite v1.53.0 ) require ( @@ -46,13 +47,13 @@ require ( github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/ebitengine/purego v0.9.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -60,11 +61,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -78,9 +75,9 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -101,21 +98,21 @@ require ( go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.56.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - modernc.org/libc v1.66.10 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect + modernc.org/libc v1.73.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 795275054e..2d0fffefbb 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/IGLOU-EU/go-wildcard/v2 v2.1.0 h1:WFqyYAuIYLJ6mHZ4rp/bYXiR4E1IvXW4+zInYWdQBqI= -github.com/IGLOU-EU/go-wildcard/v2 v2.1.0/go.mod h1:/sUMQ5dk2owR0ZcjRI/4AZ+bUFF5DxGCQrDMNBXUf5o= +github.com/IGLOU-EU/go-wildcard/v2 v2.1.1 h1:ZrH6+wwNQoHvYVfAjaXNHSnAm9p13XwCB7d4LslY1eM= +github.com/IGLOU-EU/go-wildcard/v2 v2.1.1/go.mod h1:+OfnvguiScTA1SE2N9nVfyL2B5Urwam/lrFdWcAgWNk= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= @@ -12,9 +12,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= -github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE= +github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crewjam/saml v0.5.1 h1:g+mfp0CrLuLRZCK793PgJcZeg5dS/0CDwoeAX2zcwNI= @@ -25,22 +24,22 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 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/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= -github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -60,32 +59,24 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw= github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -96,8 +87,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -115,34 +104,28 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= -github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= -github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= -github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= +github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= +github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= +github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= 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= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +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/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -169,16 +152,15 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8= github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= +github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/russellhaering/goxmldsig v1.6.0 h1:8fdWXEPh2k/NZNQBPFNoVfS3JmzS4ZprY/sAOpKQLks= github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBeYAqaHEyUeWPxZlBM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY= -github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs= +github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -202,8 +184,6 @@ github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9R github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -224,79 +204,40 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +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/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +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.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -306,49 +247,53 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= -k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= -k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= -k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= -k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= -modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= -modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= +k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= +k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= +k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= +k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= +k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= +modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= +modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= +modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= -modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= +modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= 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/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY= -modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= +modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/agenttls/config.go b/internal/agenttls/config.go index 67520b0f9e..e01ba20e71 100644 --- a/internal/agenttls/config.go +++ b/internal/agenttls/config.go @@ -1,8 +1,10 @@ package agenttls import ( + "crypto/sha256" "crypto/tls" "crypto/x509" + "encoding/hex" "fmt" "os" "strings" @@ -14,6 +16,13 @@ import ( // and optional custom CA bundle support. func NewClientTLSConfig(caBundlePath string, insecureSkipVerify bool, expectedServerFingerprint string) (*tls.Config, error) { expectedServerFingerprint = strings.TrimSpace(expectedServerFingerprint) + if expectedServerFingerprint != "" { + normalized := strings.ReplaceAll(expectedServerFingerprint, ":", "") + decoded, err := hex.DecodeString(normalized) + if err != nil || len(decoded) != sha256.Size { + return nil, fmt.Errorf("server fingerprint must be a SHA-256 value (64 hexadecimal characters)") + } + } tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} if expectedServerFingerprint != "" { diff --git a/internal/agenttls/config_test.go b/internal/agenttls/config_test.go index 406d81042b..781a25470c 100644 --- a/internal/agenttls/config_test.go +++ b/internal/agenttls/config_test.go @@ -8,6 +8,7 @@ import ( "encoding/pem" "os" "path/filepath" + "strings" "testing" ) @@ -100,3 +101,11 @@ func TestNewClientTLSConfig_UsesPinnedFingerprint(t *testing.T) { t.Fatal("expected mismatched certificate fingerprint to be rejected") } } + +func TestNewClientTLSConfig_RejectsMalformedFingerprint(t *testing.T) { + for _, fingerprint := range []string{"abc", strings.Repeat("z", 64), strings.Repeat("a", 62)} { + if _, err := NewClientTLSConfig("", false, fingerprint); err == nil { + t.Fatalf("expected malformed fingerprint %q to fail", fingerprint) + } + } +} diff --git a/internal/agentupdate/coverage_test.go b/internal/agentupdate/coverage_test.go index 130130a75d..8abf3924e3 100644 --- a/internal/agentupdate/coverage_test.go +++ b/internal/agentupdate/coverage_test.go @@ -403,6 +403,14 @@ func TestVerifyBinaryMagicOverrides(t *testing.T) { t.Fatalf("expected ELF error") } + runtimeGOOS = "freebsd" + if err := verifyBinaryMagic(elfPath); err != nil { + t.Fatalf("expected FreeBSD ELF to validate, got %v", err) + } + if err := verifyBinaryMagic(badELFPath); err == nil { + t.Fatalf("expected FreeBSD ELF error") + } + runtimeGOOS = "darwin" machoPath := filepath.Join(tmpDir, "macho") machoData := []byte{0xcf, 0xfa, 0xed, 0xfe, 0x00} @@ -443,8 +451,8 @@ func TestVerifyBinaryMagicOverrides(t *testing.T) { if err := os.WriteFile(planPath, []byte{0x00, 0x01, 0x02, 0x03}, 0644); err != nil { t.Fatalf("write plan9: %v", err) } - if err := verifyBinaryMagic(planPath); err != nil { - t.Fatalf("expected unknown OS to skip verification, got %v", err) + if err := verifyBinaryMagic(planPath); err == nil { + t.Fatalf("expected unknown OS to fail closed") } } diff --git a/internal/agentupdate/update.go b/internal/agentupdate/update.go index d0bb15cf0b..24feb934ff 100644 --- a/internal/agentupdate/update.go +++ b/internal/agentupdate/update.go @@ -61,12 +61,34 @@ const ( goOSLinux goOS = "linux" goOSDarwin goOS = "darwin" goOSWindows goOS = "windows" + goOSFreeBSD goOS = "freebsd" ) type serverVersionResponse struct { Version string `json:"version"` } +const ( + UpdateStateIdle = "idle" + UpdateStateChecking = "checking" + UpdateStateUpdateAvailable = "update-available" + UpdateStateUpdating = "updating" + UpdateStateError = "error" + UpdateStateDisabled = "disabled" +) + +// Status is a non-secret snapshot of the self-update loop suitable for agent +// reports and fleet diagnostics. +type Status struct { + State string + AutoUpdate bool + AvailableVersion string + LastCheckedAt *time.Time + LastAttemptAt *time.Time + LastSuccessAt *time.Time + LastError string +} + var ( maxBinarySizeBytes int64 = maxBinarySize runtimeGOOS = goOS(runtime.GOOS) @@ -136,6 +158,8 @@ type Updater struct { checkMu sync.Mutex checkInProgress bool + statusMu sync.RWMutex + status Status performUpdateFn func(context.Context) error selfTestFn func(context.Context, string) error @@ -167,11 +191,11 @@ func New(cfg Config) *Updater { ) tlsConfig, err := agenttls.NewClientTLSConfig(cfg.CACertPath, cfg.InsecureSkipVerify, cfg.ServerFingerprint) if err != nil { - configErr = fmt.Errorf("invalid CA bundle: %w", err) + configErr = fmt.Errorf("invalid TLS configuration: %w", err) logger.Error(). Err(err). Str("ca_cert_path", strings.TrimSpace(cfg.CACertPath)). - Msg("Invalid custom CA bundle; refusing to fall back to default TLS roots") + Msg("Invalid agent TLS configuration; refusing to create update client") } else { transport := &http.Transport{ TLSClientConfig: tlsConfig, @@ -191,6 +215,17 @@ func New(cfg Config) *Updater { client: client, logger: logger, configErr: configErr, + status: Status{ + State: UpdateStateIdle, + AutoUpdate: !cfg.Disabled, + }, + } + if cfg.Disabled { + u.status.State = UpdateStateDisabled + } + if configErr != nil { + u.status.State = UpdateStateError + u.status.LastError = configErr.Error() } u.performUpdateFn = u.performUpdate u.selfTestFn = u.runDownloadedBinarySelfTest @@ -199,14 +234,53 @@ func New(cfg Config) *Updater { return u } +// Snapshot returns a concurrency-safe copy of the current update state. +func (u *Updater) Snapshot() Status { + if u == nil { + return Status{State: UpdateStateDisabled, AutoUpdate: false} + } + u.statusMu.RLock() + defer u.statusMu.RUnlock() + return cloneStatus(u.status) +} + +func cloneStatus(status Status) Status { + status.LastCheckedAt = cloneTime(status.LastCheckedAt) + status.LastAttemptAt = cloneTime(status.LastAttemptAt) + status.LastSuccessAt = cloneTime(status.LastSuccessAt) + return status +} + +func cloneTime(value *time.Time) *time.Time { + if value == nil { + return nil + } + copy := *value + return © +} + +func (u *Updater) updateStatus(mutator func(*Status)) { + u.statusMu.Lock() + defer u.statusMu.Unlock() + mutator(&u.status) +} + // RunLoop starts the update check loop. It blocks until the context is cancelled. func (u *Updater) RunLoop(ctx context.Context) { if u.cfg.Disabled { + u.updateStatus(func(status *Status) { + status.State = UpdateStateDisabled + status.AutoUpdate = false + }) u.logger.Info().Msg("auto-update disabled") return } if u.configErr != nil { + u.updateStatus(func(status *Status) { + status.State = UpdateStateError + status.LastError = u.configErr.Error() + }) u.logger.Error().Err(u.configErr).Msg("auto-update disabled due to invalid TLS client configuration") return } @@ -274,19 +348,41 @@ func (u *Updater) CheckAndUpdate(ctx context.Context) { return } if err := u.validatePulseURL(); err != nil { + u.updateStatus(func(status *Status) { + status.State = UpdateStateError + status.LastError = err.Error() + }) u.logger.Warn().Err(err).Msg("Skipping update check - insecure or invalid Pulse URL") return } + u.updateStatus(func(status *Status) { + status.State = UpdateStateChecking + status.LastError = "" + }) u.logger.Debug().Msg("checking for agent updates") serverVersion, err := u.getServerVersion(ctx) + checkedAt := time.Now().UTC() if err != nil { + u.updateStatus(func(status *Status) { + status.State = UpdateStateError + status.LastCheckedAt = &checkedAt + status.LastError = err.Error() + }) u.logger.Warn().Err(err).Msg("failed to check for updates") return } + u.updateStatus(func(status *Status) { + status.LastCheckedAt = &checkedAt + status.AvailableVersion = strings.TrimSpace(serverVersion) + }) if serverVersion == developmentVersion { + u.updateStatus(func(status *Status) { + status.State = UpdateStateIdle + status.AvailableVersion = "" + }) u.logger.Debug().Msg("Skipping update - server is in development mode") return } @@ -298,10 +394,20 @@ func (u *Updater) CheckAndUpdate(ctx context.Context) { cmp := utils.CompareVersions(serverNorm, currentNorm) if cmp == 0 { + u.updateStatus(func(status *Status) { + status.State = UpdateStateIdle + status.AvailableVersion = "" + status.LastError = "" + }) u.logger.Debug().Str("version", u.cfg.CurrentVersion).Msg("agent is up to date") return } if cmp < 0 { + u.updateStatus(func(status *Status) { + status.State = UpdateStateIdle + status.AvailableVersion = "" + status.LastError = "" + }) u.logger.Debug(). Str("currentVersion", currentNorm). Str("serverVersion", serverNorm). @@ -313,8 +419,19 @@ func (u *Updater) CheckAndUpdate(ctx context.Context) { Str("currentVersion", u.cfg.CurrentVersion). Str("availableVersion", serverVersion). Msg("new agent version available, performing self-update") + attemptedAt := time.Now().UTC() + u.updateStatus(func(status *Status) { + status.State = UpdateStateUpdating + status.AvailableVersion = strings.TrimSpace(serverVersion) + status.LastAttemptAt = &attemptedAt + status.LastError = "" + }) if err := u.performUpdateFn(ctx); err != nil { + u.updateStatus(func(status *Status) { + status.State = UpdateStateError + status.LastError = err.Error() + }) u.logger.Error(). Err(err). Str("currentVersion", u.cfg.CurrentVersion). @@ -330,6 +447,13 @@ func (u *Updater) CheckAndUpdate(ctx context.Context) { } return } + succeededAt := time.Now().UTC() + u.updateStatus(func(status *Status) { + status.State = UpdateStateIdle + status.AvailableVersion = "" + status.LastSuccessAt = &succeededAt + status.LastError = "" + }) u.logger.Info().Msg("agent updated successfully, restarting") } @@ -540,7 +664,7 @@ func verifyBinaryMagic(path string) (retErr error) { } switch runtimeGOOS { - case goOSLinux: + case goOSLinux, goOSFreeBSD: // ELF magic: 0x7f 'E' 'L' 'F' if magic[0] == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F' { return nil @@ -568,8 +692,7 @@ func verifyBinaryMagic(path string) (retErr error) { return fmt.Errorf("verifyBinaryMagic: %s is not a valid PE binary", path) default: - // Unknown platform, skip verification - return nil + return fmt.Errorf("verifyBinaryMagic: executable validation is unsupported on %s", runtimeGOOS) } } diff --git a/internal/agentupdate/update_http_test.go b/internal/agentupdate/update_http_test.go index 91565052ef..1b1e2568e1 100644 --- a/internal/agentupdate/update_http_test.go +++ b/internal/agentupdate/update_http_test.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" + "errors" "net/http" "net/http/httptest" "os" @@ -149,7 +150,7 @@ func TestNew_UsesPinnedServerFingerprintForHTTPTransport(t *testing.T) { u := New(Config{ PulseURL: "https://pulse.example.com", CurrentVersion: "1.0.0", - ServerFingerprint: "aabbccdd", + ServerFingerprint: "aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd", }) transport, ok := u.client.Transport.(*http.Transport) @@ -185,7 +186,7 @@ func TestNew_InvalidCustomCABundleFailsClosed(t *testing.T) { if u.configErr == nil { t.Fatal("expected invalid CA bundle to populate configErr") } - if _, err := u.getServerVersion(context.Background()); err == nil || !strings.Contains(err.Error(), "invalid CA bundle") { + if _, err := u.getServerVersion(context.Background()); err == nil || !strings.Contains(err.Error(), "invalid TLS configuration") { t.Fatalf("expected invalid CA bundle error, got %v", err) } } @@ -327,6 +328,52 @@ func TestUpdater_CheckAndUpdate_VersionComparePaths(t *testing.T) { } } +func TestUpdater_CheckAndUpdate_RecordsLifecycleStatus(t *testing.T) { + t.Run("current", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(serverVersionResponse{Version: "1.0.0"}) + })) + defer srv.Close() + + u := New(Config{PulseURL: srv.URL, CurrentVersion: "1.0.0"}) + u.CheckAndUpdate(context.Background()) + + status := u.Snapshot() + if status.State != UpdateStateIdle || !status.AutoUpdate || status.LastCheckedAt == nil { + t.Fatalf("current status = %+v", status) + } + if status.AvailableVersion != "" || status.LastError != "" { + t.Fatalf("current status retained transient fields: %+v", status) + } + }) + + t.Run("failed update", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(serverVersionResponse{Version: "1.1.0"}) + })) + defer srv.Close() + + u := New(Config{PulseURL: srv.URL, CurrentVersion: "1.0.0"}) + u.performUpdateFn = func(context.Context) error { return errors.New("replacement rejected") } + u.CheckAndUpdate(context.Background()) + + status := u.Snapshot() + if status.State != UpdateStateError || status.AvailableVersion != "1.1.0" { + t.Fatalf("failed update status = %+v", status) + } + if status.LastCheckedAt == nil || status.LastAttemptAt == nil || status.LastError != "replacement rejected" { + t.Fatalf("failed update evidence = %+v", status) + } + }) + + t.Run("disabled", func(t *testing.T) { + status := New(Config{Disabled: true}).Snapshot() + if status.State != UpdateStateDisabled || status.AutoUpdate { + t.Fatalf("disabled status = %+v", status) + } + }) +} + func TestUpdater_performUpdateWithExecPath_RejectsRedirects(t *testing.T) { var redirectedHits int32 redirectTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/agentupdate/update_test.go b/internal/agentupdate/update_test.go index 0d7dafe8d6..a7029e30b1 100644 --- a/internal/agentupdate/update_test.go +++ b/internal/agentupdate/update_test.go @@ -17,7 +17,7 @@ func TestDetermineArch(t *testing.T) { // On known platforms, should return os-arch format switch runtime.GOOS { - case "linux", "darwin", "windows": + case "linux", "darwin", "windows", "freebsd": if result == "" { t.Errorf("determineArch() returned empty string on %s/%s", runtime.GOOS, runtime.GOARCH) } diff --git a/internal/api/connections_aggregator.go b/internal/api/connections_aggregator.go index 357901ae95..d63598df8c 100644 --- a/internal/api/connections_aggregator.go +++ b/internal/api/connections_aggregator.go @@ -41,6 +41,8 @@ const ( fleetStateEnabled = "enabled" fleetStateEnrolled = "enrolled" fleetStateHealthy = "healthy" + fleetStateChecking = "checking" + fleetStateFailed = "failed" fleetStateInvalid = "invalid" fleetStateNotApplicable = "not-applicable" fleetStatePaused = "paused" @@ -48,6 +50,7 @@ const ( fleetStateReported = "reported" fleetStateUnknown = "unknown" fleetStateUpdateAvailable = "update-available" + fleetStateUpdating = "updating" fleetStateVerified = "verified" fleetConfigDriftCurrent = "current" @@ -531,15 +534,58 @@ func buildAgentConnection(host models.Host, expectedAgentVersion string, now tim AgentVersion: currentAgentVersion, ExpectedAgentVersion: expectedAgentVersion, AgentUpdateAvailable: updateAvailable, + AgentUpdate: connectionAgentUpdateStatus(host.AgentUpdate), + AgentModules: connectionAgentModuleStatuses(host.AgentModules), Capabilities: ConnectionCapabilities{SupportsPause: false, SupportsScope: false, SupportsTest: false}, }, now) - conn.Fleet.ConfigDrift = connectionFleetAgentConfigDrift(conn, desiredConfig) + conn.Fleet.ConfigDrift = connectionFleetAgentConfigDrift(conn, desiredConfig, host.AppliedConfig) conn.Fleet.CredentialHealth = connectionFleetAgentCredentialHealth(conn, host, now) conn.Fleet.CommandPolicy = connectionFleetAgentCommandPolicy(conn, host, desiredConfig) conn.Fleet.Rollout = connectionFleetRollout(conn) return conn } +func connectionAgentModuleStatuses(values []models.AgentModuleStatus) []ConnectionAgentModuleStatus { + if len(values) == 0 { + return nil + } + result := make([]ConnectionAgentModuleStatus, 0, len(values)) + for _, value := range values { + result = append(result, ConnectionAgentModuleStatus{ + Name: strings.TrimSpace(value.Name), + Enabled: value.Enabled, + State: strings.TrimSpace(value.State), + LastError: strings.TrimSpace(value.LastError), + UpdatedAt: value.UpdatedAt.UTC(), + }) + } + return result +} + +func connectionAgentUpdateStatus(value *models.AgentUpdateStatus) *ConnectionAgentUpdateStatus { + if value == nil { + return nil + } + return &ConnectionAgentUpdateStatus{ + State: strings.TrimSpace(value.State), + AutoUpdate: value.AutoUpdate, + UpdatedFrom: strings.TrimSpace(value.UpdatedFrom), + AvailableVersion: strings.TrimSpace(value.AvailableVersion), + LastCheckedAt: cloneTime(value.LastCheckedAt), + LastAttemptAt: cloneTime(value.LastAttemptAt), + LastSuccessAt: cloneTime(value.LastSuccessAt), + LastError: strings.TrimSpace(value.LastError), + } +} + +func cloneTime(value *time.Time) *time.Time { + if value == nil { + return nil + } + copy := value.UTC() + return © +} + func withFleetGovernance(conn Connection, now time.Time) Connection { conn.Fleet = deriveConnectionFleetGovernance(conn, now) return conn @@ -590,6 +636,13 @@ func connectionFleetVersionDrift(conn Connection) string { } func connectionFleetAdapterHealth(conn Connection) string { + if conn.Type == ConnectionTypeAgent { + for _, module := range conn.AgentModules { + if module.Enabled && strings.TrimSpace(module.State) != "running" { + return fleetStateDegraded + } + } + } switch conn.State { case ConnectionStateActive: return fleetStateHealthy @@ -634,6 +687,20 @@ func connectionFleetUpdateStatus(conn Connection) string { if conn.Type != ConnectionTypeAgent { return fleetStateNotApplicable } + if conn.AgentUpdate != nil { + switch strings.TrimSpace(conn.AgentUpdate.State) { + case "error": + return fleetStateFailed + case "updating": + return fleetStateUpdating + case "checking": + return fleetStateChecking + case "disabled": + if conn.AgentUpdateAvailable { + return fleetStateDisabled + } + } + } if conn.AgentUpdateAvailable { return fleetStateUpdateAvailable } @@ -685,7 +752,7 @@ func connectionFleetConfigDrift(conn Connection) *ConnectionFleetConfigDrift { } } -func connectionFleetAgentConfigDrift(conn Connection, desiredConfig *connectionAgentDesiredConfig) *ConnectionFleetConfigDrift { +func connectionFleetAgentConfigDrift(conn Connection, desiredConfig *connectionAgentDesiredConfig, appliedConfig *models.AgentConfigFingerprint) *ConnectionFleetConfigDrift { if desiredConfig == nil { return &ConnectionFleetConfigDrift{ Status: fleetStateUnknown, @@ -699,7 +766,14 @@ func connectionFleetAgentConfigDrift(conn Connection, desiredConfig *connectionA Reason: "no managed agent configuration override is assigned", } } - return connectionFleetAgentConfigDriftForFingerprints(conn, desiredConfig.Fingerprint, nil) + return connectionFleetAgentConfigDriftForFingerprints(conn, desiredConfig.Fingerprint, connectionConfigFingerprintFromAgent(appliedConfig)) +} + +func connectionConfigFingerprintFromAgent(value *models.AgentConfigFingerprint) *ConnectionFleetConfigFingerprint { + if value == nil { + return nil + } + return connectionConfigFingerprintFromMetadata(value.Version, value.Hash) } func connectionFleetAgentConfigDriftForFingerprints(conn Connection, desired, applied *ConnectionFleetConfigFingerprint) *ConnectionFleetConfigDrift { diff --git a/internal/api/connections_aggregator_test.go b/internal/api/connections_aggregator_test.go index 265b8f3c1f..b130cce6fb 100644 --- a/internal/api/connections_aggregator_test.go +++ b/internal/api/connections_aggregator_test.go @@ -330,8 +330,18 @@ func TestBuildConnections_AgentVersionUpdateAvailability(t *testing.T) { now := time.Now() in := aggregatorInputs{ hosts: []models.Host{ - {ID: "current", Hostname: "current", LastSeen: now, AgentVersion: "6.0.3"}, - {ID: "outdated", Hostname: "outdated", LastSeen: now, AgentVersion: "6.0.0"}, + {ID: "current", Hostname: "current", LastSeen: now, AgentVersion: "6.0.3", AgentUpdate: &models.AgentUpdateStatus{State: "disabled"}}, + {ID: "outdated", Hostname: "outdated", LastSeen: now, AgentVersion: "6.0.0", AgentUpdate: &models.AgentUpdateStatus{State: "disabled"}}, + { + ID: "failed", + Hostname: "failed", + LastSeen: now, + AgentVersion: "6.0.3", + AgentUpdate: &models.AgentUpdateStatus{State: "error", LastError: "signature verification failed"}, + AgentModules: []models.AgentModuleStatus{{ + Name: "docker", Enabled: true, State: "retrying", LastError: "socket unavailable", UpdatedAt: now, + }}, + }, {ID: "unknown", Hostname: "unknown", LastSeen: now}, }, expectedAgentVersion: "6.0.3", @@ -357,6 +367,9 @@ func TestBuildConnections_AgentVersionUpdateAvailability(t *testing.T) { if byID["agent:current"].AgentUpdateAvailable { t.Fatal("current agent should not report an update available") } + if byID["agent:current"].Fleet.UpdateStatus != fleetStateCurrent { + t.Fatalf("current agent update status = %q, want current", byID["agent:current"].Fleet.UpdateStatus) + } if byID["agent:outdated"].AgentVersion != "6.0.0" { t.Fatalf( @@ -368,6 +381,16 @@ func TestBuildConnections_AgentVersionUpdateAvailability(t *testing.T) { if !byID["agent:outdated"].AgentUpdateAvailable { t.Fatal("outdated agent should report an update available") } + if byID["agent:outdated"].Fleet.UpdateStatus != fleetStateDisabled { + t.Fatalf("outdated disabled agent update status = %q, want disabled", byID["agent:outdated"].Fleet.UpdateStatus) + } + + if byID["agent:failed"].Fleet.UpdateStatus != fleetStateFailed || byID["agent:failed"].AgentUpdate == nil || byID["agent:failed"].AgentUpdate.LastError != "signature verification failed" { + t.Fatalf("failed agent update status = %+v", byID["agent:failed"]) + } + if byID["agent:failed"].Fleet.AdapterHealth != fleetStateDegraded || len(byID["agent:failed"].AgentModules) != 1 { + t.Fatalf("failed agent module status = %+v", byID["agent:failed"]) + } if byID["agent:unknown"].AgentVersion != "" { t.Fatalf("unknown agent version = %q, want empty", byID["agent:unknown"].AgentVersion) @@ -389,6 +412,10 @@ func TestBuildConnections_AgentFleetGovernance(t *testing.T) { LastSeen: now, AgentVersion: "6.0.3", CommandsEnabled: true, + AppliedConfig: &models.AgentConfigFingerprint{ + Version: currentDesired.Version, + Hash: currentDesired.Hash, + }, }, { ID: "outdated", @@ -426,14 +453,15 @@ func TestBuildConnections_AgentFleetGovernance(t *testing.T) { t.Fatalf("current agent fleet governance = %+v", current) } if current.ConfigDrift == nil || - current.ConfigDrift.Status != fleetStatePending || + current.ConfigDrift.Status != fleetConfigDriftCurrent || current.ConfigDrift.Desired == nil || - current.ConfigDrift.Applied != nil || + current.ConfigDrift.Applied == nil || current.ConfigDrift.Desired.Version != connectionAgentConfigFingerprintVersion || - current.ConfigDrift.Desired.Hash != currentDesired.Hash { + current.ConfigDrift.Desired.Hash != currentDesired.Hash || + current.ConfigDrift.Applied.Hash != currentDesired.Hash { t.Fatalf("current agent config drift = %+v", current.ConfigDrift) } - if current.Rollout == nil || current.Rollout.Status != fleetStatePending || current.Rollout.Stage != fleetRolloutStagePending { + if current.Rollout == nil || current.Rollout.Status != fleetStateCurrent || current.Rollout.Stage != fleetRolloutStageApplied { t.Fatalf("current agent rollout = %+v", current.Rollout) } if current.CommandPolicy == nil || diff --git a/internal/api/connections_types.go b/internal/api/connections_types.go index 1640de611f..a46ca2e44d 100644 --- a/internal/api/connections_types.go +++ b/internal/api/connections_types.go @@ -107,6 +107,25 @@ type ConnectionFleetCommandPolicy struct { Reason string `json:"reason,omitempty"` } +type ConnectionAgentUpdateStatus struct { + State string `json:"state"` + AutoUpdate bool `json:"autoUpdate"` + UpdatedFrom string `json:"updatedFrom,omitempty"` + AvailableVersion string `json:"availableVersion,omitempty"` + LastCheckedAt *time.Time `json:"lastCheckedAt,omitempty"` + LastAttemptAt *time.Time `json:"lastAttemptAt,omitempty"` + LastSuccessAt *time.Time `json:"lastSuccessAt,omitempty"` + LastError string `json:"lastError,omitempty"` +} + +type ConnectionAgentModuleStatus struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + State string `json:"state"` + LastError string `json:"lastError,omitempty"` + UpdatedAt time.Time `json:"updatedAt"` +} + // ConnectionError is the runtime error shape surfaced on a connection row. // Mirrors monitoring.ErrorDetail but lives in the api package so the type // stays stable if the internal monitoring shape evolves. @@ -136,25 +155,27 @@ type ConnectionAgentIdentity struct { // per-type shapes that today require separate fetches and separate table // renderers. type Connection struct { - ID string `json:"id"` - Type ConnectionType `json:"type"` - Name string `json:"name"` - Address string `json:"address"` - HostAliases []string `json:"hostAliases,omitempty"` - State ConnectionState `json:"state"` - StateReason string `json:"stateReason,omitempty"` - Enabled bool `json:"enabled"` - Surfaces []string `json:"surfaces"` - Scope map[string]bool `json:"scope"` - LastSeen *time.Time `json:"lastSeen,omitempty"` - LastError *ConnectionError `json:"lastError,omitempty"` - Source ConnectionSource `json:"source"` - AgentIdentity *ConnectionAgentIdentity `json:"agentIdentity,omitempty"` - AgentVersion string `json:"agentVersion,omitempty"` - ExpectedAgentVersion string `json:"expectedAgentVersion,omitempty"` - AgentUpdateAvailable bool `json:"agentUpdateAvailable,omitempty"` - Fleet ConnectionFleetGovernance `json:"fleet"` - Capabilities ConnectionCapabilities `json:"capabilities"` + ID string `json:"id"` + Type ConnectionType `json:"type"` + Name string `json:"name"` + Address string `json:"address"` + HostAliases []string `json:"hostAliases,omitempty"` + State ConnectionState `json:"state"` + StateReason string `json:"stateReason,omitempty"` + Enabled bool `json:"enabled"` + Surfaces []string `json:"surfaces"` + Scope map[string]bool `json:"scope"` + LastSeen *time.Time `json:"lastSeen,omitempty"` + LastError *ConnectionError `json:"lastError,omitempty"` + Source ConnectionSource `json:"source"` + AgentIdentity *ConnectionAgentIdentity `json:"agentIdentity,omitempty"` + AgentVersion string `json:"agentVersion,omitempty"` + ExpectedAgentVersion string `json:"expectedAgentVersion,omitempty"` + AgentUpdateAvailable bool `json:"agentUpdateAvailable,omitempty"` + AgentUpdate *ConnectionAgentUpdateStatus `json:"agentUpdate,omitempty"` + AgentModules []ConnectionAgentModuleStatus `json:"agentModules,omitempty"` + Fleet ConnectionFleetGovernance `json:"fleet"` + Capabilities ConnectionCapabilities `json:"capabilities"` } type ConnectionSystemComponentRole string diff --git a/internal/dockeragent/agent.go b/internal/dockeragent/agent.go index dc3d49e201..68279c74a3 100644 --- a/internal/dockeragent/agent.go +++ b/internal/dockeragent/agent.go @@ -3,7 +3,6 @@ package dockeragent import ( "bytes" "context" - "crypto/tls" "encoding/json" "errors" "fmt" @@ -16,6 +15,7 @@ import ( systemtypes "github.com/moby/moby/api/types/system" "github.com/moby/moby/client" + "github.com/rcourtman/pulse-go-rewrite/internal/agenttls" "github.com/rcourtman/pulse-go-rewrite/internal/securityutil" "github.com/rcourtman/pulse-go-rewrite/internal/utils" agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker" @@ -27,6 +27,8 @@ type TargetConfig struct { URL string Token string InsecureSkipVerify bool + CACertPath string + ServerFingerprint string } // Config describes runtime configuration for the Docker / Podman collection module. @@ -39,6 +41,8 @@ type Config struct { AgentType string // "unified" when running as part of pulse-agent, empty for legacy standalone mode AgentVersion string // Version to report; if empty, uses dockeragent.Version InsecureSkipVerify bool + CACertPath string + ServerFingerprint string DisableAutoUpdate bool DisableUpdateChecks bool // Disable Docker image update detection (registry checks) Targets []TargetConfig @@ -108,6 +112,7 @@ type Agent struct { agentVersion string supportsSwarm bool httpClients map[bool]*http.Client + trustedHTTPClients map[string]*http.Client logger zerolog.Logger machineID string hostName string @@ -160,6 +165,8 @@ func New(cfg Config) (*Agent, error) { URL: url, Token: token, InsecureSkipVerify: cfg.InsecureSkipVerify, + CACertPath: cfg.CACertPath, + ServerFingerprint: cfg.ServerFingerprint, }}) if err != nil { return nil, fmt.Errorf("dockeragent.New: normalize fallback target: %w", err) @@ -170,6 +177,8 @@ func New(cfg Config) (*Agent, error) { cfg.PulseURL = targets[0].URL cfg.APIToken = targets[0].Token cfg.InsecureSkipVerify = targets[0].InsecureSkipVerify + cfg.CACertPath = targets[0].CACertPath + cfg.ServerFingerprint = targets[0].ServerFingerprint stateFilters, err := normalizeContainerStates(cfg.ContainerStates) if err != nil { @@ -241,12 +250,27 @@ func New(cfg Config) (*Agent, error) { } httpClients := make(map[bool]*http.Client, 2) + trustedHTTPClients := make(map[string]*http.Client) if hasSecure { httpClients[false] = newHTTPClient(false) } if hasInsecure { httpClients[true] = newHTTPClient(true) } + for _, target := range cfg.Targets { + if target.CACertPath == "" && target.ServerFingerprint == "" { + continue + } + key := targetTrustKey(target) + if _, exists := trustedHTTPClients[key]; exists { + continue + } + client, err := newHTTPClientWithTrust(target.CACertPath, target.InsecureSkipVerify, target.ServerFingerprint) + if err != nil { + return nil, fmt.Errorf("configure TLS for Pulse target %s: %w", target.URL, err) + } + trustedHTTPClients[key] = client + } machineID, _ := readMachineID() hostName := cfg.HostnameOverride @@ -265,24 +289,25 @@ func New(cfg Config) (*Agent, error) { const bufferCapacity = 60 agent := &Agent{ - cfg: cfg, - docker: dockerClient, - daemonHost: dockerClient.DaemonHost(), - daemonID: info.ID, // Cache at init for stable agent ID - runtime: runtimeKind, - runtimeVer: info.ServerVersion, - agentVersion: agentVersion, - supportsSwarm: runtimeKind == RuntimeDocker, - httpClients: httpClients, - logger: *logger, - machineID: machineID, - hostName: hostName, - targets: cfg.Targets, - allowedStates: make(map[string]struct{}, len(stateFilters)), - stateFilters: stateFilters, - prevContainerCPU: make(map[string]cpuSample), - reportBuffer: utils.New[agentsdocker.Report](bufferCapacity), - registryChecker: newRegistryCheckerWithConfig(*logger, !cfg.DisableUpdateChecks), + cfg: cfg, + docker: dockerClient, + daemonHost: dockerClient.DaemonHost(), + daemonID: info.ID, // Cache at init for stable agent ID + runtime: runtimeKind, + runtimeVer: info.ServerVersion, + agentVersion: agentVersion, + supportsSwarm: runtimeKind == RuntimeDocker, + httpClients: httpClients, + trustedHTTPClients: trustedHTTPClients, + logger: *logger, + machineID: machineID, + hostName: hostName, + targets: cfg.Targets, + allowedStates: make(map[string]struct{}, len(stateFilters)), + stateFilters: stateFilters, + prevContainerCPU: make(map[string]cpuSample), + reportBuffer: utils.New[agentsdocker.Report](bufferCapacity), + registryChecker: newRegistryCheckerWithConfig(*logger, !cfg.DisableUpdateChecks), } for _, state := range stateFilters { @@ -321,7 +346,9 @@ func normalizeTargets(raw []TargetConfig) ([]TargetConfig, error) { return nil, fmt.Errorf("invalid pulse target URL %q: %w", targetURL, err) } - key := fmt.Sprintf("%s|%s|%t", normalizedURL, token, target.InsecureSkipVerify) + caCertPath := strings.TrimSpace(target.CACertPath) + serverFingerprint := strings.TrimSpace(target.ServerFingerprint) + key := fmt.Sprintf("%s|%s|%t|%s|%s", normalizedURL, token, target.InsecureSkipVerify, caCertPath, serverFingerprint) if _, exists := seen[key]; exists { continue } @@ -331,6 +358,8 @@ func normalizeTargets(raw []TargetConfig) ([]TargetConfig, error) { URL: normalizedURL, Token: token, InsecureSkipVerify: target.InsecureSkipVerify, + CACertPath: caCertPath, + ServerFingerprint: serverFingerprint, }) } @@ -1149,6 +1178,9 @@ func (a *Agent) primaryTarget() TargetConfig { } func (a *Agent) httpClientFor(target TargetConfig) *http.Client { + if client, ok := a.trustedHTTPClients[targetTrustKey(target)]; ok { + return client + } if client, ok := a.httpClients[target.InsecureSkipVerify]; ok { return client } @@ -1161,12 +1193,22 @@ func (a *Agent) httpClientFor(target TargetConfig) *http.Client { return newHTTPClient(target.InsecureSkipVerify) } +func targetTrustKey(target TargetConfig) string { + return fmt.Sprintf("%t|%s|%s", target.InsecureSkipVerify, strings.TrimSpace(target.CACertPath), strings.TrimSpace(target.ServerFingerprint)) +} + func newHTTPClient(insecure bool) *http.Client { - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, + client, err := newHTTPClientWithTrust("", insecure, "") + if err != nil { + panic(fmt.Sprintf("build default Pulse HTTP client: %v", err)) } - if insecure { - tlsConfig.InsecureSkipVerify = true //nolint:gosec + return client +} + +func newHTTPClientWithTrust(caCertPath string, insecure bool, serverFingerprint string) (*http.Client, error) { + tlsConfig, err := agenttls.NewClientTLSConfig(caCertPath, insecure, serverFingerprint) + if err != nil { + return nil, err } return &http.Client{ @@ -1180,7 +1222,7 @@ func newHTTPClient(insecure bool) *http.Client { CheckRedirect: func(req *http.Request, via []*http.Request) error { return fmt.Errorf("server returned redirect to %s - if using a reverse proxy, ensure you use the correct protocol (https:// instead of http://) in your --url flag", req.URL) }, - } + }, nil } func (a *Agent) Close() error { @@ -1207,6 +1249,11 @@ func (a *Agent) Close() error { client.CloseIdleConnections() } } + for _, client := range a.trustedHTTPClients { + if client != nil { + client.CloseIdleConnections() + } + } if a.registryChecker != nil && a.registryChecker.httpClient != nil { a.registryChecker.httpClient.CloseIdleConnections() } diff --git a/internal/dockeragent/agent_collect_test.go b/internal/dockeragent/agent_collect_test.go index 986ab3a2bc..2ee3b31872 100644 --- a/internal/dockeragent/agent_collect_test.go +++ b/internal/dockeragent/agent_collect_test.go @@ -6,6 +6,8 @@ import ( "io" "net/http" "net/netip" + "os" + "path/filepath" "strings" "testing" "time" @@ -586,6 +588,20 @@ func TestPrimaryTargetAndHTTPClient(t *testing.T) { } }) + t.Run("custom trust client selection", func(t *testing.T) { + custom := &http.Client{} + target := TargetConfig{CACertPath: "/etc/pulse/ca.pem", ServerFingerprint: "abc"} + agent := &Agent{ + httpClients: map[bool]*http.Client{false: {}}, + trustedHTTPClients: map[string]*http.Client{ + targetTrustKey(target): custom, + }, + } + if got := agent.httpClientFor(target); got != custom { + t.Fatal("expected target-specific trust client") + } + }) + t.Run("http client fallback", func(t *testing.T) { agent := &Agent{ httpClients: map[bool]*http.Client{}, @@ -634,6 +650,16 @@ func TestNewHTTPClient(t *testing.T) { } } +func TestNewHTTPClientWithTrustRejectsInvalidCABundle(t *testing.T) { + path := filepath.Join(t.TempDir(), "invalid-ca.pem") + if err := os.WriteFile(path, []byte("not a certificate"), 0o600); err != nil { + t.Fatalf("write invalid CA: %v", err) + } + if _, err := newHTTPClientWithTrust(path, false, ""); err == nil { + t.Fatal("expected invalid custom CA bundle to fail") + } +} + func TestAgentClose(t *testing.T) { closed := false agent := &Agent{ diff --git a/internal/dockeragent/agent_internal_test.go b/internal/dockeragent/agent_internal_test.go index 82e6138344..8e6faeb209 100644 --- a/internal/dockeragent/agent_internal_test.go +++ b/internal/dockeragent/agent_internal_test.go @@ -16,8 +16,8 @@ import ( func TestNormalizeTargets(t *testing.T) { targets, err := normalizeTargets([]TargetConfig{ - {URL: " https://pulse.example.com/ ", Token: "tokenA", InsecureSkipVerify: false}, - {URL: "https://pulse.example.com", Token: "tokenA", InsecureSkipVerify: false}, // duplicate + {URL: " https://pulse.example.com/ ", Token: "tokenA", InsecureSkipVerify: false, CACertPath: " /etc/pulse/ca.pem ", ServerFingerprint: " AB:CD "}, + {URL: "https://pulse.example.com", Token: "tokenA", InsecureSkipVerify: false, CACertPath: "/etc/pulse/ca.pem", ServerFingerprint: "AB:CD"}, // duplicate {URL: "https://pulse-dr.example.com", Token: "tokenB", InsecureSkipVerify: true}, }) if err != nil { @@ -28,7 +28,7 @@ func TestNormalizeTargets(t *testing.T) { t.Fatalf("expected 2 targets, got %d", len(targets)) } - if targets[0].URL != "https://pulse.example.com" || targets[0].Token != "tokenA" || targets[0].InsecureSkipVerify { + if targets[0].URL != "https://pulse.example.com" || targets[0].Token != "tokenA" || targets[0].InsecureSkipVerify || targets[0].CACertPath != "/etc/pulse/ca.pem" || targets[0].ServerFingerprint != "AB:CD" { t.Fatalf("unexpected first target: %+v", targets[0]) } diff --git a/internal/hostagent/agent.go b/internal/hostagent/agent.go index 85fdef2e44..affa7737f2 100644 --- a/internal/hostagent/agent.go +++ b/internal/hostagent/agent.go @@ -22,6 +22,7 @@ import ( "github.com/rcourtman/pulse-go-rewrite/internal/agenttls" "github.com/rcourtman/pulse-go-rewrite/internal/agentupdate" "github.com/rcourtman/pulse-go-rewrite/internal/platformsupport" + "github.com/rcourtman/pulse-go-rewrite/internal/remoteconfig" "github.com/rcourtman/pulse-go-rewrite/internal/securityutil" "github.com/rcourtman/pulse-go-rewrite/internal/sensors" "github.com/rcourtman/pulse-go-rewrite/internal/utils" @@ -70,6 +71,13 @@ type Config struct { ReportIP string // IP address to report instead of auto-detected (for multi-NIC systems) DisableCeph bool // If true, disables local Ceph status polling + // AppliedConfig is the non-secret fingerprint of the managed config that + // was applied before this runtime started. UpdateStatus supplies live + // self-update state without coupling report collection to updater internals. + AppliedConfig *agentshost.ConfigFingerprint + UpdateStatus func() agentupdate.Status + ModuleStatus func() []agentshost.ModuleStatus + Collector SystemCollector // Optional: override default system information collector (for testing) newCommandClientFn func(Config, string, string, string, string) *CommandClient @@ -255,7 +263,7 @@ func New(cfg Config) (*Agent, error) { } tlsConfig, err := agenttls.NewClientTLSConfig(cfg.CACertPath, cfg.InsecureSkipVerify, cfg.ServerFingerprint) if err != nil { - return nil, fmt.Errorf("invalid CA bundle: %w", err) + return nil, fmt.Errorf("invalid TLS configuration: %w", err) } client := &http.Client{ @@ -457,6 +465,7 @@ type runtimeConfigSnapshot struct { tags []string reportIP string disableCeph bool + appliedConfig *agentshost.ConfigFingerprint } func (a *Agent) currentInterval() time.Duration { @@ -485,9 +494,41 @@ func (a *Agent) runtimeConfigSnapshot() runtimeConfigSnapshot { tags: append([]string(nil), a.cfg.Tags...), reportIP: a.reportIP, disableCeph: a.cfg.DisableCeph, + appliedConfig: cloneConfigFingerprint(a.cfg.AppliedConfig), } } +func cloneConfigFingerprint(value *agentshost.ConfigFingerprint) *agentshost.ConfigFingerprint { + if value == nil { + return nil + } + copy := *value + return © +} + +func (a *Agent) currentUpdateStatus() *agentshost.UpdateStatus { + if a.cfg.UpdateStatus == nil { + return nil + } + status := a.cfg.UpdateStatus() + return &agentshost.UpdateStatus{ + State: status.State, + AutoUpdate: status.AutoUpdate, + AvailableVersion: status.AvailableVersion, + LastCheckedAt: status.LastCheckedAt, + LastAttemptAt: status.LastAttemptAt, + LastSuccessAt: status.LastSuccessAt, + LastError: status.LastError, + } +} + +func (a *Agent) currentModuleStatus() []agentshost.ModuleStatus { + if a.cfg.ModuleStatus == nil { + return nil + } + return append([]agentshost.ModuleStatus(nil), a.cfg.ModuleStatus()...) +} + func (a *Agent) signalRemoteConfigChanged() { select { case a.remoteConfigChanged <- struct{}{}: @@ -803,6 +844,9 @@ func (a *Agent) buildReport(ctx context.Context) (agentshost.Report, error) { UpdatedFrom: updatedFrom, CommandsEnabled: runtimeConfig.commandsEnabled, DiskExclude: append([]string(nil), runtimeConfig.diskExclude...), + AppliedConfig: runtimeConfig.appliedConfig, + Update: a.currentUpdateStatus(), + Modules: a.currentModuleStatus(), }, Host: agentshost.HostInfo{ ID: a.machineID, @@ -966,6 +1010,28 @@ func (a *Agent) ApplyRemoteConfig(settings map[string]interface{}, commandsEnabl a.configMu.Unlock() a.logger.Info().Bool("disable_ceph", disableCeph).Msg("Applied remote Ceph collection setting") } + if remoteConfigAppliedWithoutRestart(settings) && remoteconfig.HasAppliedDesiredConfig(commandsEnabled, settings) { + metadata, err := remoteconfig.BuildDesiredConfigMetadata(commandsEnabled, settings) + if err != nil { + a.logger.Warn().Err(err).Msg("Failed to derive refreshed managed configuration fingerprint") + return + } + a.configMu.Lock() + a.cfg.AppliedConfig = &agentshost.ConfigFingerprint{Version: metadata.Version, Hash: metadata.Hash} + a.configMu.Unlock() + } +} + +func remoteConfigAppliedWithoutRestart(settings map[string]interface{}) bool { + for key := range settings { + switch key { + case "interval", "report_ip", "disable_ceph": + continue + default: + return false + } + } + return true } // applyRemoteConfig applies server-side command execution overrides. diff --git a/internal/hostagent/agent_metrics_test.go b/internal/hostagent/agent_metrics_test.go index 6c96e65336..ef17656b48 100644 --- a/internal/hostagent/agent_metrics_test.go +++ b/internal/hostagent/agent_metrics_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/rcourtman/pulse-go-rewrite/internal/agentupdate" "github.com/rcourtman/pulse-go-rewrite/internal/hostmetrics" agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host" gohost "github.com/shirou/gopsutil/v4/host" @@ -107,10 +108,17 @@ func TestBuildReport(t *testing.T) { // Create Agent with mock cfg := Config{ - AgentID: "agent-123", - APIToken: "test-token", - LogLevel: -1, - Collector: mc, + AgentID: "agent-123", + APIToken: "test-token", + LogLevel: -1, + Collector: mc, + AppliedConfig: &agentshost.ConfigFingerprint{Version: "v1", Hash: "sha256:test"}, + UpdateStatus: func() agentupdate.Status { + return agentupdate.Status{State: agentupdate.UpdateStateIdle, AutoUpdate: true} + }, + ModuleStatus: func() []agentshost.ModuleStatus { + return []agentshost.ModuleStatus{{Name: "host", Enabled: true, State: "running", UpdatedAt: fixedTime}} + }, } agent, err := New(cfg) if err != nil { @@ -128,6 +136,15 @@ func TestBuildReport(t *testing.T) { if report.Agent.ID != "agent-123" { t.Errorf("Agent.ID = %q, want %q", report.Agent.ID, "agent-123") } + if report.Agent.AppliedConfig == nil || report.Agent.AppliedConfig.Hash != "sha256:test" { + t.Fatalf("Agent.AppliedConfig = %+v", report.Agent.AppliedConfig) + } + if report.Agent.Update == nil || report.Agent.Update.State != agentupdate.UpdateStateIdle || !report.Agent.Update.AutoUpdate { + t.Fatalf("Agent.Update = %+v", report.Agent.Update) + } + if len(report.Agent.Modules) != 1 || report.Agent.Modules[0].Name != "host" || report.Agent.Modules[0].State != "running" { + t.Fatalf("Agent.Modules = %+v", report.Agent.Modules) + } // Verify Host Info if report.Host.Hostname != "test-host" { diff --git a/internal/hostagent/agent_new_test.go b/internal/hostagent/agent_new_test.go index 46a77ff342..f21842ad82 100644 --- a/internal/hostagent/agent_new_test.go +++ b/internal/hostagent/agent_new_test.go @@ -17,6 +17,7 @@ import ( "github.com/prometheus/client_golang/prometheus/testutil" "github.com/rcourtman/pulse-go-rewrite/internal/agentexec" "github.com/rcourtman/pulse-go-rewrite/internal/hostmetrics" + "github.com/rcourtman/pulse-go-rewrite/internal/remoteconfig" agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host" "github.com/rs/zerolog" gohost "github.com/shirou/gopsutil/v4/host" @@ -471,7 +472,7 @@ func TestNewCommandClient_SetsSecureCommandDefaults(t *testing.T) { client := NewCommandClient(Config{ PulseURL: "https://pulse.example", APIToken: "runtime-token", - ServerFingerprint: "aabbccdd", + ServerFingerprint: "aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd", DeploySSHUser: "pulse-deploy", Logger: &logger, }, "agent-1", "node-1", "linux", "1.0.0") @@ -482,8 +483,8 @@ func TestNewCommandClient_SetsSecureCommandDefaults(t *testing.T) { if client.stateDir != defaultStateDir { t.Fatalf("stateDir = %q, want %q", client.stateDir, defaultStateDir) } - if client.serverFingerprint != "aabbccdd" { - t.Fatalf("serverFingerprint = %q, want %q", client.serverFingerprint, "aabbccdd") + if client.serverFingerprint != "aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd" { + t.Fatalf("serverFingerprint = %q, want configured SHA-256 pin", client.serverFingerprint) } if client.deploySSHUser != "pulse-deploy" { t.Fatalf("deploySSHUser = %q, want %q", client.deploySSHUser, "pulse-deploy") @@ -537,6 +538,17 @@ func TestAgentApplyRemoteConfigUpdatesRuntimeSnapshot(t *testing.T) { if !snapshot.disableCeph { t.Fatal("disableCeph = false, want true") } + expectedMetadata, err := remoteconfig.BuildDesiredConfigMetadata(&commandsEnabled, map[string]interface{}{ + "interval": "45s", + "report_ip": "192.0.2.20", + "disable_ceph": true, + }) + if err != nil { + t.Fatalf("BuildDesiredConfigMetadata: %v", err) + } + if snapshot.appliedConfig == nil || snapshot.appliedConfig.Version != expectedMetadata.Version || snapshot.appliedConfig.Hash != expectedMetadata.Hash { + t.Fatalf("applied config = %+v, want %+v", snapshot.appliedConfig, expectedMetadata) + } select { case <-agent.remoteConfigChanged: default: @@ -544,6 +556,22 @@ func TestAgentApplyRemoteConfigUpdatesRuntimeSnapshot(t *testing.T) { } } +func TestAgentApplyRemoteConfigKeepsPriorFingerprintWhenRestartIsRequired(t *testing.T) { + prior := &agentshost.ConfigFingerprint{Version: "host-agent-config/v1", Hash: "sha256:prior"} + agent := &Agent{ + cfg: Config{AppliedConfig: prior}, + remoteConfigChanged: make(chan struct{}, 1), + logger: zerolog.Nop(), + } + + agent.ApplyRemoteConfig(map[string]interface{}{"enable_docker": true}, nil) + + snapshot := agent.runtimeConfigSnapshot() + if snapshot.appliedConfig == nil || snapshot.appliedConfig.Hash != prior.Hash { + t.Fatalf("restart-required config should retain prior applied fingerprint, got %+v", snapshot.appliedConfig) + } +} + func TestNewRejectsInvalidDeploySSHUser(t *testing.T) { mc := &mockCollector{ hostInfoFn: func(context.Context) (*gohost.InfoStat, error) { @@ -619,7 +647,7 @@ func TestNew_UsesPinnedServerFingerprintForHTTPTransport(t *testing.T) { agent, err := New(Config{ PulseURL: "https://pulse.example", APIToken: "token", - ServerFingerprint: "aabbccdd", + ServerFingerprint: "aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd", LogLevel: zerolog.InfoLevel, Collector: mc, }) diff --git a/internal/kubernetesagent/agent.go b/internal/kubernetesagent/agent.go index d379463227..4e4f1f6fa7 100644 --- a/internal/kubernetesagent/agent.go +++ b/internal/kubernetesagent/agent.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "crypto/sha256" - "crypto/tls" "encoding/hex" "encoding/json" "errors" @@ -20,6 +19,7 @@ import ( "time" "github.com/IGLOU-EU/go-wildcard/v2" + "github.com/rcourtman/pulse-go-rewrite/internal/agenttls" "github.com/rcourtman/pulse-go-rewrite/internal/securityutil" "github.com/rcourtman/pulse-go-rewrite/internal/utils" agentsk8s "github.com/rcourtman/pulse-go-rewrite/pkg/agents/kubernetes" @@ -55,6 +55,8 @@ type Config struct { AgentType string // "unified" when running as part of pulse-agent AgentVersion string // Version to report; if empty, uses kubernetesagent.Version InsecureSkipVerify bool + CACertPath string + ServerFingerprint string LogLevel zerolog.Level Logger *zerolog.Logger @@ -181,10 +183,9 @@ func New(cfg Config) (*Agent, error) { agentVersion = Version } - tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} - if cfg.InsecureSkipVerify { - //nolint:gosec // Insecure mode is explicitly user-controlled. - tlsConfig.InsecureSkipVerify = true + tlsConfig, err := agenttls.NewClientTLSConfig(cfg.CACertPath, cfg.InsecureSkipVerify, cfg.ServerFingerprint) + if err != nil { + return nil, fmt.Errorf("configure Pulse TLS client: %w", err) } httpClient := &http.Client{ Timeout: 15 * time.Second, diff --git a/internal/kubernetesagent/agent_new_test.go b/internal/kubernetesagent/agent_new_test.go index 312481a10c..4fb2ed0415 100644 --- a/internal/kubernetesagent/agent_new_test.go +++ b/internal/kubernetesagent/agent_new_test.go @@ -106,6 +106,30 @@ func TestNew_WithKubeconfig(t *testing.T) { } } +func TestNew_RejectsInvalidPulseCABundle(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/version" { + _, _ = w.Write([]byte(`{"gitVersion":"v1.36.2"}`)) + return + } + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + tmp := t.TempDir() + kubeconfigPath := filepath.Join(tmp, "config") + writeTestKubeconfig(t, kubeconfigPath, server.URL, "ctx-ca") + caPath := filepath.Join(tmp, "invalid-ca.pem") + if err := os.WriteFile(caPath, []byte("not a certificate"), 0o600); err != nil { + t.Fatalf("write invalid CA: %v", err) + } + + _, err := New(Config{APIToken: "token", KubeconfigPath: kubeconfigPath, CACertPath: caPath}) + if err == nil || !strings.Contains(err.Error(), "configure Pulse TLS client") { + t.Fatalf("expected invalid Pulse CA error, got %v", err) + } +} + func TestNormalizePulseURL(t *testing.T) { tests := []struct { name string diff --git a/internal/models/converters.go b/internal/models/converters.go index a9ec8ee2ee..7885af5cd8 100644 --- a/internal/models/converters.go +++ b/internal/models/converters.go @@ -508,6 +508,9 @@ func (h Host) ToFrontend() HostFrontend { Tags: append([]string(nil), h.Tags...), LastSeen: timeToUnixMillis(h.LastSeen), CommandsEnabled: h.CommandsEnabled, + AppliedConfig: cloneAgentConfigFingerprint(h.AppliedConfig), + AgentUpdate: cloneAgentUpdateStatus(h.AgentUpdate), + AgentModules: cloneAgentModuleStatuses(h.AgentModules), IsLegacy: h.IsLegacy, LinkedNodeID: h.LinkedNodeID, } @@ -548,6 +551,41 @@ func (h Host) ToFrontend() HostFrontend { return host.NormalizeCollections() } +func cloneAgentConfigFingerprint(value *AgentConfigFingerprint) *AgentConfigFingerprint { + if value == nil { + return nil + } + copy := *value + return © +} + +func cloneAgentUpdateStatus(value *AgentUpdateStatus) *AgentUpdateStatus { + if value == nil { + return nil + } + copy := *value + if value.LastCheckedAt != nil { + checked := *value.LastCheckedAt + copy.LastCheckedAt = &checked + } + if value.LastAttemptAt != nil { + attempt := *value.LastAttemptAt + copy.LastAttemptAt = &attempt + } + if value.LastSuccessAt != nil { + success := *value.LastSuccessAt + copy.LastSuccessAt = &success + } + return © +} + +func cloneAgentModuleStatuses(values []AgentModuleStatus) []AgentModuleStatus { + if len(values) == 0 { + return nil + } + return append([]AgentModuleStatus(nil), values...) +} + // ToFrontend converts a DockerContainer to DockerContainerFrontend func (c DockerContainer) ToFrontend() DockerContainerFrontend { container := DockerContainerFrontend{ diff --git a/internal/models/models.go b/internal/models/models.go index 8f420c0d01..edf3ee162f 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -247,40 +247,43 @@ func (c Container) NormalizeCollections() Container { // Host represents a generic infrastructure host reporting via external agents. type Host struct { - ID string `json:"id"` - Hostname string `json:"hostname"` - DisplayName string `json:"displayName,omitempty"` - Platform string `json:"platform,omitempty"` - OSName string `json:"osName,omitempty"` - OSVersion string `json:"osVersion,omitempty"` - KernelVersion string `json:"kernelVersion,omitempty"` - Architecture string `json:"architecture,omitempty"` - CPUCount int `json:"cpuCount,omitempty"` - CPUUsage float64 `json:"cpuUsage,omitempty"` - Memory Memory `json:"memory"` - LoadAverage []float64 `json:"loadAverage,omitempty"` - Disks []Disk `json:"disks,omitempty"` - DiskIO []DiskIO `json:"diskIO,omitempty"` - NetworkInterfaces []HostNetworkInterface `json:"networkInterfaces,omitempty"` - Sensors HostSensorSummary `json:"sensors,omitempty"` - RAID []HostRAIDArray `json:"raid,omitempty"` - Unraid *HostUnraidStorage `json:"unraid,omitempty"` - Ceph *HostCephCluster `json:"ceph,omitempty"` - Status string `json:"status"` - UptimeSeconds int64 `json:"uptimeSeconds,omitempty"` - IntervalSeconds int `json:"intervalSeconds,omitempty"` - LastSeen time.Time `json:"lastSeen"` - AgentVersion string `json:"agentVersion,omitempty"` - MachineID string `json:"machineId,omitempty"` - CommandsEnabled bool `json:"commandsEnabled,omitempty"` // Whether AI command execution is enabled - ReportIP string `json:"reportIp,omitempty"` // User-specified IP for multi-NIC systems - TokenID string `json:"tokenId,omitempty"` - TokenName string `json:"tokenName,omitempty"` - TokenHint string `json:"tokenHint,omitempty"` - TokenLastUsedAt *time.Time `json:"tokenLastUsedAt,omitempty"` - Tags []string `json:"tags,omitempty"` - DiskExclude []string `json:"diskExclude,omitempty"` // Agent's --disk-exclude patterns - IsLegacy bool `json:"isLegacy,omitempty"` + ID string `json:"id"` + Hostname string `json:"hostname"` + DisplayName string `json:"displayName,omitempty"` + Platform string `json:"platform,omitempty"` + OSName string `json:"osName,omitempty"` + OSVersion string `json:"osVersion,omitempty"` + KernelVersion string `json:"kernelVersion,omitempty"` + Architecture string `json:"architecture,omitempty"` + CPUCount int `json:"cpuCount,omitempty"` + CPUUsage float64 `json:"cpuUsage,omitempty"` + Memory Memory `json:"memory"` + LoadAverage []float64 `json:"loadAverage,omitempty"` + Disks []Disk `json:"disks,omitempty"` + DiskIO []DiskIO `json:"diskIO,omitempty"` + NetworkInterfaces []HostNetworkInterface `json:"networkInterfaces,omitempty"` + Sensors HostSensorSummary `json:"sensors,omitempty"` + RAID []HostRAIDArray `json:"raid,omitempty"` + Unraid *HostUnraidStorage `json:"unraid,omitempty"` + Ceph *HostCephCluster `json:"ceph,omitempty"` + Status string `json:"status"` + UptimeSeconds int64 `json:"uptimeSeconds,omitempty"` + IntervalSeconds int `json:"intervalSeconds,omitempty"` + LastSeen time.Time `json:"lastSeen"` + AgentVersion string `json:"agentVersion,omitempty"` + MachineID string `json:"machineId,omitempty"` + CommandsEnabled bool `json:"commandsEnabled,omitempty"` // Whether AI command execution is enabled + ReportIP string `json:"reportIp,omitempty"` // User-specified IP for multi-NIC systems + TokenID string `json:"tokenId,omitempty"` + TokenName string `json:"tokenName,omitempty"` + TokenHint string `json:"tokenHint,omitempty"` + TokenLastUsedAt *time.Time `json:"tokenLastUsedAt,omitempty"` + Tags []string `json:"tags,omitempty"` + DiskExclude []string `json:"diskExclude,omitempty"` // Agent's --disk-exclude patterns + AppliedConfig *AgentConfigFingerprint `json:"appliedConfig,omitempty"` + AgentUpdate *AgentUpdateStatus `json:"agentUpdate,omitempty"` + AgentModules []AgentModuleStatus `json:"agentModules,omitempty"` + IsLegacy bool `json:"isLegacy,omitempty"` // Computed I/O rates (bytes/sec), populated from cumulative counters by rate tracker NetInRate float64 `json:"netInRate,omitempty"` @@ -294,6 +297,30 @@ type Host struct { LinkedContainerID string `json:"linkedContainerId,omitempty"` // ID of the container this agent is running inside } +type AgentConfigFingerprint struct { + Version string `json:"version"` + Hash string `json:"hash"` +} + +type AgentUpdateStatus struct { + State string `json:"state"` + AutoUpdate bool `json:"autoUpdate"` + UpdatedFrom string `json:"updatedFrom,omitempty"` + AvailableVersion string `json:"availableVersion,omitempty"` + LastCheckedAt *time.Time `json:"lastCheckedAt,omitempty"` + LastAttemptAt *time.Time `json:"lastAttemptAt,omitempty"` + LastSuccessAt *time.Time `json:"lastSuccessAt,omitempty"` + LastError string `json:"lastError,omitempty"` +} + +type AgentModuleStatus struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + State string `json:"state"` + LastError string `json:"lastError,omitempty"` + UpdatedAt time.Time `json:"updatedAt"` +} + func (h Host) NormalizeCollections() Host { if h.LoadAverage == nil { h.LoadAverage = []float64{} diff --git a/internal/models/models_frontend.go b/internal/models/models_frontend.go index 3e36645b1b..6a09242a42 100644 --- a/internal/models/models_frontend.go +++ b/internal/models/models_frontend.go @@ -721,8 +721,11 @@ type HostFrontend struct { TokenLastUsedAt *int64 `json:"tokenLastUsedAt,omitempty"` Tags []string `json:"tags"` CommandsEnabled bool `json:"commandsEnabled,omitempty"` // Whether AI command execution is enabled - IsLegacy bool `json:"isLegacy,omitempty"` // True if using legacy agent protocol - LinkedNodeID string `json:"linkedNodeId,omitempty"` // ID of linked PVE node (if running on a node) + AppliedConfig *AgentConfigFingerprint `json:"appliedConfig,omitempty"` + AgentUpdate *AgentUpdateStatus `json:"agentUpdate,omitempty"` + AgentModules []AgentModuleStatus `json:"agentModules,omitempty"` + IsLegacy bool `json:"isLegacy,omitempty"` // True if using legacy agent protocol + LinkedNodeID string `json:"linkedNodeId,omitempty"` // ID of linked PVE node (if running on a node) } func (h HostFrontend) NormalizeCollections() HostFrontend { diff --git a/internal/monitoring/monitor_agents.go b/internal/monitoring/monitor_agents.go index 972ecedb7d..f0bce80c75 100644 --- a/internal/monitoring/monitor_agents.go +++ b/internal/monitoring/monitor_agents.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/rcourtman/pulse-go-rewrite/internal/agentupdate" "github.com/rcourtman/pulse-go-rewrite/internal/config" "github.com/rcourtman/pulse-go-rewrite/internal/logging" "github.com/rcourtman/pulse-go-rewrite/internal/models" @@ -1947,6 +1948,13 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config. } } + agentUpdate := mergeAgentUpdateStatus( + previousHostAgentUpdate(m.state.GetHosts(), identifier), + convertAgentUpdateStatus(report.Agent.Update), + strings.TrimSpace(report.Agent.UpdatedFrom), + observedAt, + ) + host := models.Host{ ID: identifier, Hostname: hostname, @@ -1985,6 +1993,9 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config. ReportIP: strings.TrimSpace(report.Host.ReportIP), Tags: append([]string(nil), report.Tags...), DiskExclude: append([]string(nil), report.Agent.DiskExclude...), + AppliedConfig: convertAgentConfigFingerprint(report.Agent.AppliedConfig), + AgentUpdate: agentUpdate, + AgentModules: convertAgentModuleStatuses(report.Agent.Modules), IsLegacy: isLegacyAgent(report.Agent.Type), } @@ -2199,6 +2210,107 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config. return host, nil } +func convertAgentConfigFingerprint(value *agentshost.ConfigFingerprint) *models.AgentConfigFingerprint { + if value == nil { + return nil + } + return &models.AgentConfigFingerprint{ + Version: strings.TrimSpace(value.Version), + Hash: strings.TrimSpace(value.Hash), + } +} + +func convertAgentUpdateStatus(value *agentshost.UpdateStatus) *models.AgentUpdateStatus { + if value == nil { + return nil + } + return &models.AgentUpdateStatus{ + State: strings.TrimSpace(value.State), + AutoUpdate: value.AutoUpdate, + UpdatedFrom: strings.TrimSpace(value.UpdatedFrom), + AvailableVersion: strings.TrimSpace(value.AvailableVersion), + LastCheckedAt: cloneAgentStatusTime(value.LastCheckedAt), + LastAttemptAt: cloneAgentStatusTime(value.LastAttemptAt), + LastSuccessAt: cloneAgentStatusTime(value.LastSuccessAt), + LastError: strings.TrimSpace(value.LastError), + } +} + +func convertAgentModuleStatuses(values []agentshost.ModuleStatus) []models.AgentModuleStatus { + if len(values) == 0 { + return nil + } + result := make([]models.AgentModuleStatus, 0, len(values)) + for _, value := range values { + name := strings.TrimSpace(value.Name) + if name == "" { + continue + } + result = append(result, models.AgentModuleStatus{ + Name: name, + Enabled: value.Enabled, + State: strings.TrimSpace(value.State), + LastError: strings.TrimSpace(value.LastError), + UpdatedAt: value.UpdatedAt.UTC(), + }) + } + return result +} + +func previousHostAgentUpdate(hosts []models.Host, identifier string) *models.AgentUpdateStatus { + for i := range hosts { + if hosts[i].ID == identifier { + return cloneModelAgentUpdateStatus(hosts[i].AgentUpdate) + } + } + return nil +} + +func mergeAgentUpdateStatus(previous, reported *models.AgentUpdateStatus, updatedFrom string, observedAt time.Time) *models.AgentUpdateStatus { + if reported == nil { + reported = cloneModelAgentUpdateStatus(previous) + } + if reported == nil && updatedFrom == "" { + return nil + } + if reported == nil { + reported = &models.AgentUpdateStatus{State: agentupdate.UpdateStateIdle, AutoUpdate: true} + } + if previous != nil { + if reported.LastSuccessAt == nil { + reported.LastSuccessAt = cloneAgentStatusTime(previous.LastSuccessAt) + } + if reported.UpdatedFrom == "" { + reported.UpdatedFrom = previous.UpdatedFrom + } + } + if updatedFrom != "" { + reported.UpdatedFrom = updatedFrom + succeededAt := observedAt.UTC() + reported.LastSuccessAt = &succeededAt + } + return reported +} + +func cloneModelAgentUpdateStatus(value *models.AgentUpdateStatus) *models.AgentUpdateStatus { + if value == nil { + return nil + } + copy := *value + copy.LastCheckedAt = cloneAgentStatusTime(value.LastCheckedAt) + copy.LastAttemptAt = cloneAgentStatusTime(value.LastAttemptAt) + copy.LastSuccessAt = cloneAgentStatusTime(value.LastSuccessAt) + return © +} + +func cloneAgentStatusTime(value *time.Time) *time.Time { + if value == nil { + return nil + } + copy := value.UTC() + return © +} + func hostPrimaryTemperatureCelsius(sensors models.HostSensorSummary) *float64 { if len(sensors.TemperatureCelsius) == 0 { return nil diff --git a/internal/monitoring/monitor_host_agents_test.go b/internal/monitoring/monitor_host_agents_test.go index 141dfc0ca0..4a78a9abec 100644 --- a/internal/monitoring/monitor_host_agents_test.go +++ b/internal/monitoring/monitor_host_agents_test.go @@ -827,6 +827,63 @@ func TestApplyHostReportUsesReceiptTimeForSkewedAgentClockLiveness(t *testing.T) } } +func TestApplyHostReportPreservesAgentLifecycleEvidence(t *testing.T) { + monitor := newTestMonitor(t) + checkedAt := time.Now().UTC().Add(-time.Minute).Truncate(time.Second) + report := agentshost.Report{ + Agent: agentshost.AgentInfo{ + ID: "lifecycle-agent", + Version: "6.0.4", + UpdatedFrom: "6.0.3", + AppliedConfig: &agentshost.ConfigFingerprint{ + Version: "pulse-agent-config-v1", + Hash: "sha256:applied", + }, + Update: &agentshost.UpdateStatus{ + State: "error", + AutoUpdate: true, + AvailableVersion: "6.0.5", + LastCheckedAt: &checkedAt, + LastError: "signature verification failed", + }, + Modules: []agentshost.ModuleStatus{{ + Name: "docker", Enabled: true, State: "retrying", LastError: "socket unavailable", UpdatedAt: checkedAt, + }}, + }, + Host: agentshost.HostInfo{ID: "machine-lifecycle", Hostname: "lifecycle.local", Platform: "linux"}, + } + + host, err := monitor.ApplyHostReport(report, &config.APITokenRecord{ID: "lifecycle-token"}) + if err != nil { + t.Fatalf("ApplyHostReport: %v", err) + } + if host.AppliedConfig == nil || host.AppliedConfig.Version != "pulse-agent-config-v1" || host.AppliedConfig.Hash != "sha256:applied" { + t.Fatalf("applied config = %+v", host.AppliedConfig) + } + if host.AgentUpdate == nil || host.AgentUpdate.State != "error" || host.AgentUpdate.LastCheckedAt == nil || !host.AgentUpdate.LastCheckedAt.Equal(checkedAt) { + t.Fatalf("agent update = %+v", host.AgentUpdate) + } + if host.AgentUpdate.LastError != "signature verification failed" { + t.Fatalf("last update error = %q", host.AgentUpdate.LastError) + } + if len(host.AgentModules) != 1 || host.AgentModules[0].Name != "docker" || host.AgentModules[0].LastError != "socket unavailable" { + t.Fatalf("agent modules = %+v", host.AgentModules) + } + if host.AgentUpdate.UpdatedFrom != "6.0.3" || host.AgentUpdate.LastSuccessAt == nil { + t.Fatalf("successful restart evidence = %+v", host.AgentUpdate) + } + + report.Agent.UpdatedFrom = "" + report.Agent.Update = &agentshost.UpdateStatus{State: "idle", AutoUpdate: true} + host, err = monitor.ApplyHostReport(report, &config.APITokenRecord{ID: "lifecycle-token"}) + if err != nil { + t.Fatalf("ApplyHostReport second report: %v", err) + } + if host.AgentUpdate == nil || host.AgentUpdate.UpdatedFrom != "6.0.3" || host.AgentUpdate.LastSuccessAt == nil { + t.Fatalf("second report lost successful restart evidence: %+v", host.AgentUpdate) + } +} + func TestApplyDockerReportUsesReceiptTimeForSkewedAgentClockLiveness(t *testing.T) { monitor := newTestMonitor(t) diff --git a/internal/remoteconfig/client.go b/internal/remoteconfig/client.go index 05c65ddeb5..853f9a7d3a 100644 --- a/internal/remoteconfig/client.go +++ b/internal/remoteconfig/client.go @@ -79,8 +79,8 @@ func New(cfg Config) *Client { cfg.Logger.Error(). Err(err). Str("ca_cert_path", strings.TrimSpace(cfg.CACertPath)). - Msg("Invalid custom CA bundle; refusing to fall back to default TLS roots") - configErr = errors.Join(configErr, fmt.Errorf("invalid CA bundle: %w", err)) + Msg("Invalid agent TLS configuration; refusing to create remote config client") + configErr = errors.Join(configErr, fmt.Errorf("invalid TLS configuration: %w", err)) } else { httpClient = &http.Client{ Timeout: 10 * time.Second, diff --git a/internal/remoteconfig/client_additional_test.go b/internal/remoteconfig/client_additional_test.go index 12cee20394..fd55a86b67 100644 --- a/internal/remoteconfig/client_additional_test.go +++ b/internal/remoteconfig/client_additional_test.go @@ -331,7 +331,7 @@ func TestClientNewDefaultsAndAgentLookupNotFound(t *testing.T) { func TestClientNew_UsesPinnedServerFingerprint(t *testing.T) { client := New(Config{ PulseURL: "https://pulse.example", - ServerFingerprint: "aabbccdd", + ServerFingerprint: "aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd", }) transport, ok := client.httpClient.Transport.(*http.Transport) @@ -365,7 +365,7 @@ func TestClientNew_InvalidCustomCABundleFailsClosed(t *testing.T) { if client.httpClient != nil { t.Fatal("expected HTTP client to remain disabled when the custom CA bundle is invalid") } - if _, _, err := client.Fetch(context.Background()); err == nil || !strings.Contains(err.Error(), "invalid CA bundle") { + if _, _, err := client.Fetch(context.Background()); err == nil || !strings.Contains(err.Error(), "invalid TLS configuration") { t.Fatalf("expected invalid CA bundle error, got %v", err) } } diff --git a/pkg/agents/host/report.go b/pkg/agents/host/report.go index 8b5ae61b70..7cda5aef00 100644 --- a/pkg/agents/host/report.go +++ b/pkg/agents/host/report.go @@ -31,14 +31,50 @@ type ClusterNodeSensors struct { // AgentInfo describes the reporting agent. type AgentInfo struct { - ID string `json:"id"` - Version string `json:"version,omitempty"` - Type string `json:"type,omitempty"` // "unified", "host", or "docker" - empty means legacy - IntervalSeconds int `json:"intervalSeconds,omitempty"` - Hostname string `json:"hostname,omitempty"` - UpdatedFrom string `json:"updatedFrom,omitempty"` // Previous version if recently auto-updated - CommandsEnabled bool `json:"commandsEnabled,omitempty"` // Whether AI command execution is enabled - DiskExclude []string `json:"diskExclude,omitempty"` // Disk exclusion patterns from --disk-exclude flag + ID string `json:"id"` + Version string `json:"version,omitempty"` + Type string `json:"type,omitempty"` // "unified", "host", or "docker" - empty means legacy + IntervalSeconds int `json:"intervalSeconds,omitempty"` + Hostname string `json:"hostname,omitempty"` + UpdatedFrom string `json:"updatedFrom,omitempty"` // Previous version if recently auto-updated + CommandsEnabled bool `json:"commandsEnabled,omitempty"` // Whether AI command execution is enabled + DiskExclude []string `json:"diskExclude,omitempty"` // Disk exclusion patterns from --disk-exclude flag + AppliedConfig *ConfigFingerprint `json:"appliedConfig,omitempty"` + Update *UpdateStatus `json:"update,omitempty"` + Modules []ModuleStatus `json:"modules,omitempty"` +} + +// ModuleStatus describes whether an enabled Unified Agent module initialized +// and is actively running. Errors are deliberately limited to the latest +// operator-facing initialization failure. +type ModuleStatus struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + State string `json:"state"` + LastError string `json:"lastError,omitempty"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// ConfigFingerprint identifies the effective managed configuration applied by +// the running agent without exposing any configuration values or secrets. +type ConfigFingerprint struct { + Version string `json:"version"` + Hash string `json:"hash"` +} + +// UpdateStatus is the agent-authored state of its self-update loop. Server-side +// version comparison remains authoritative for whether an update is available; +// this payload explains whether the agent is actually checking and why its last +// attempt succeeded, failed, or is disabled. +type UpdateStatus struct { + State string `json:"state"` + AutoUpdate bool `json:"autoUpdate"` + UpdatedFrom string `json:"updatedFrom,omitempty"` + AvailableVersion string `json:"availableVersion,omitempty"` + LastCheckedAt *time.Time `json:"lastCheckedAt,omitempty"` + LastAttemptAt *time.Time `json:"lastAttemptAt,omitempty"` + LastSuccessAt *time.Time `json:"lastSuccessAt,omitempty"` + LastError string `json:"lastError,omitempty"` } // HostInfo contains platform and identification details about the monitored host. diff --git a/scripts/.go-version b/scripts/.go-version index 90fef721dd..9c89591a3c 100644 --- a/scripts/.go-version +++ b/scripts/.go-version @@ -1 +1 @@ -go1.25.11 +go1.26.5 diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 23da3414da..a18f49af9f 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -19,7 +19,7 @@ if [ -x /usr/local/go/bin/go ]; then fi # Release artifacts must be built with the vetted toolchain to match security-gate evidence. -required_go="go1.25.11" +required_go="go1.26.5" current_go="$(go env GOVERSION 2>/dev/null || true)" if [[ "${PULSE_SKIP_GO_VERSION_CHECK:-false}" != "true" ]]; then if [[ "${current_go}" != "${required_go}" ]]; then @@ -147,6 +147,29 @@ for i in "${!agent_build_order[@]}"; do ./cmd/pulse-agent done +# Platform-native signing jobs may supply replacement desktop binaries. They +# are copied before packaging, checksums, SBOM generation, and release signing +# so the immutable candidate manifest covers the exact signed bytes users get. +if [[ -n "${PULSE_AGENT_NATIVE_BINARIES_DIR:-}" ]]; then + native_dir="${PULSE_AGENT_NATIVE_BINARIES_DIR}" + native_targets=(darwin-amd64 darwin-arm64 windows-amd64 windows-arm64 windows-386) + for target in "${native_targets[@]}"; do + filename="pulse-agent-${target}" + if [[ "$target" == windows-* ]]; then + filename="${filename}.exe" + fi + if [[ ! -f "${native_dir}/${filename}" ]]; then + echo "Error: native agent binary override is missing ${filename}." >&2 + exit 1 + fi + cp "${native_dir}/${filename}" "${BUILD_DIR}/${filename}" + done + echo "Applied platform-native signed Unified Agent binaries." +elif [[ "${PULSE_REQUIRE_PLATFORM_SIGNING:-false}" == "true" ]]; then + echo "Error: platform signing is required but PULSE_AGENT_NATIVE_BINARIES_DIR is empty." >&2 + exit 1 +fi + # Build pulse-mcp (Model Context Protocol adapter) for the same # multi-OS matrix as the unified agent. The MCP server runs on # the integrator's machine (Mac, Windows, Linux desktop) and diff --git a/scripts/install-go-toolchain.sh b/scripts/install-go-toolchain.sh index 1694d986a2..bde6c8e019 100755 --- a/scripts/install-go-toolchain.sh +++ b/scripts/install-go-toolchain.sh @@ -3,7 +3,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" VERSION_FILE="${SCRIPT_DIR}/.go-version" -DEFAULT_VERSION="go1.25.11" +DEFAULT_VERSION="go1.26.5" TARGET_ROOT="/opt/toolchains/go" DOWNLOAD_ROOT="https://dl.google.com/go" GOPATH_DIR="/var/lib/pulse/go" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index e548867ff8..0958615751 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -19,6 +19,7 @@ param ( [bool]$Insecure = $false, [bool]$Uninstall = $false, [string]$CACertPath = $env:PULSE_CACERT, + [string]$ServerFingerprint = $env:PULSE_SERVER_FINGERPRINT, [string]$AgentId = $env:PULSE_AGENT_ID, [string]$Hostname = $env:PULSE_HOSTNAME, [string]$TokenFile = $env:PULSE_TOKEN_FILE, @@ -376,10 +377,24 @@ function Invoke-WithOptionalInsecureTls { ) $previousCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback - if ($AllowInsecure -or $null -ne $CustomCaCertificate) { + if ($AllowInsecure -or $null -ne $CustomCaCertificate -or -not [string]::IsNullOrWhiteSpace($ServerFingerprint)) { [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { param($sender, $certificate, $chain, $sslPolicyErrors) + if (-not [string]::IsNullOrWhiteSpace($ServerFingerprint)) { + try { + $sha256 = [System.Security.Cryptography.SHA256]::Create() + try { + $actualFingerprint = ([System.BitConverter]::ToString($sha256.ComputeHash($certificate.GetRawCertData()))).Replace('-', '').ToLowerInvariant() + } finally { + $sha256.Dispose() + } + return [string]::Equals($actualFingerprint, $ServerFingerprint, [System.StringComparison]::OrdinalIgnoreCase) + } catch { + return $false + } + } + if ($AllowInsecure) { return $true } @@ -395,7 +410,7 @@ function Invoke-WithOptionalInsecureTls { try { & $Action } finally { - if ($AllowInsecure -or $null -ne $CustomCaCertificate) { + if ($AllowInsecure -or $null -ne $CustomCaCertificate -or -not [string]::IsNullOrWhiteSpace($ServerFingerprint)) { [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $previousCallback } } @@ -418,6 +433,9 @@ function Save-ConnectionState { if (-not [string]::IsNullOrWhiteSpace($CACertPath)) { $lines += "PULSE_CACERT='$CACertPath'" } + if (-not [string]::IsNullOrWhiteSpace($ServerFingerprint)) { + $lines += "PULSE_SERVER_FINGERPRINT='$ServerFingerprint'" + } New-Item -ItemType Directory -Path $StateDir -Force | Out-Null Set-Content -Path $ConnectionStatePath -Value ($lines -join "`n") -Encoding UTF8 @@ -485,6 +503,9 @@ if ($Uninstall) { if ([string]::IsNullOrWhiteSpace($CACertPath)) { $CACertPath = Get-ConnectionStateValue "PULSE_CACERT" } + if ([string]::IsNullOrWhiteSpace($ServerFingerprint)) { + $ServerFingerprint = Get-ConnectionStateValue "PULSE_SERVER_FINGERPRINT" + } $customCaCertificate = $null if (-not [string]::IsNullOrWhiteSpace($CACertPath)) { @@ -637,6 +658,15 @@ if (-not [string]::IsNullOrWhiteSpace($NormalizedProxmoxType) -and $NormalizedPr # Normalize URL (remove trailing slash) $Url = $Url.TrimEnd('/') +$ServerFingerprint = ($ServerFingerprint -replace '[:\s]', '').ToLowerInvariant() +if (-not [string]::IsNullOrWhiteSpace($ServerFingerprint) -and $ServerFingerprint -notmatch '^[a-f0-9]{64}$') { + Show-Error "Invalid server certificate fingerprint. Expected 64 hexadecimal SHA-256 characters." + Exit 1 +} +if (-not [string]::IsNullOrWhiteSpace($ServerFingerprint) -and -not $Url.ToLowerInvariant().StartsWith("https://")) { + Show-Error "Server certificate fingerprint pinning requires an https:// Pulse URL." + Exit 1 +} if ($Url.ToLowerInvariant().StartsWith("http://") -and -not $Insecure) { Write-Host "Plain HTTP Pulse URL detected; enabling insecure mode for persisted agent update checks." -ForegroundColor Yellow $Insecure = $true @@ -902,6 +932,7 @@ if (-not [string]::IsNullOrWhiteSpace($NormalizedProxmoxType)) { $ServiceArgs += if ($EnableCommands) { $ServiceArgs += "--enable-commands" } if ($Insecure) { $ServiceArgs += "--insecure" } if (-not [string]::IsNullOrWhiteSpace($CACertPath)) { $ServiceArgs += @("--cacert", "`"$CACertPath`"") } +if (-not [string]::IsNullOrWhiteSpace($ServerFingerprint)) { $ServiceArgs += @("--server-fingerprint", "`"$ServerFingerprint`"") } if (-not [string]::IsNullOrWhiteSpace($AgentId)) { $ServiceArgs += @("--agent-id", "`"$AgentId`"") } if (-not [string]::IsNullOrWhiteSpace($Hostname)) { $ServiceArgs += @("--hostname", "`"$Hostname`"") } diff --git a/scripts/install.sh b/scripts/install.sh index 7e572f2a6b..72ff98969c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -22,6 +22,7 @@ # --agent-id Custom agent identifier (default: auto-generated) # --disk-exclude Exclude mount points matching pattern (repeatable) # --insecure Skip TLS certificate verification +# --server-fingerprint Pin the Pulse server leaf certificate # --enable-commands Enable Pulse command execution on agent (disabled by default; required for Patrol actions and Proxmox LXC Docker inventory) # --health-addr Health/metrics listener address (default: 127.0.0.1:9191, use "" to disable) # --update Update an existing agent using saved connection state @@ -77,6 +78,7 @@ PROXMOX_TYPE="" UPDATE_ONLY="false" UNINSTALL="false" INSECURE="false" +SERVER_FINGERPRINT="${PULSE_SERVER_FINGERPRINT:-}" AGENT_ID="" HOSTNAME_OVERRIDE="" ENABLE_COMMANDS="false" @@ -301,6 +303,7 @@ Options: --disk-exclude Exclude mount point (repeatable) --insecure Skip TLS verification (auto-enabled for http:// URLs) --cacert Custom CA certificate for TLS (used by curl and agent) + --server-fingerprint Pin the Pulse server leaf certificate for agent connections --enable-commands Enable Pulse command execution (disabled by default; required for Patrol actions and Proxmox LXC Docker inventory) --health-addr Health/metrics listener address (default: 127.0.0.1:9191; use "" to disable) --enroll Exchange bootstrap token for runtime token (deploy wizard) @@ -1345,6 +1348,55 @@ auto_enable_insecure_for_plain_http_url() { log_info "Plain HTTP Pulse URL detected; enabling insecure mode for installer downloads and persisted agent update checks." } +verify_pinned_server_certificate() { + if [[ -z "$SERVER_FINGERPRINT" ]]; then + return 0 + fi + if pulse_url_uses_plain_http "$PULSE_URL"; then + fail "--server-fingerprint requires an https:// Pulse URL." "$EXIT_PREFLIGHT_FAILED" + fi + if ! command -v openssl >/dev/null 2>&1; then + fail "--server-fingerprint requires openssl so the installer can verify the server certificate before downloading." "$EXIT_PREFLIGHT_FAILED" + fi + + local normalized="" + local authority="" + local host="" + local target="" + local actual="" + normalized=$(printf '%s' "$SERVER_FINGERPRINT" | tr -d ':[:space:]' | tr '[:upper:]' '[:lower:]') + if [[ ! "$normalized" =~ ^[a-f0-9]{64}$ ]]; then + fail "Invalid --server-fingerprint value. Expected a SHA-256 certificate fingerprint (64 hexadecimal characters)." "$EXIT_PREFLIGHT_FAILED" + fi + + authority="${PULSE_URL#https://}" + authority="${authority%%/*}" + if [[ "$authority" == \[*\]* ]]; then + host="${authority#\[}" + host="${host%%\]*}" + target="$authority" + if [[ "$authority" != *"]:"* ]]; then target="${authority}:443"; fi + else + host="${authority%%:*}" + target="$authority" + if [[ "$authority" != *:* ]]; then target="${authority}:443"; fi + fi + + actual=$(openssl s_client -connect "$target" -servername "$host" /dev/null \ + | openssl x509 -outform DER 2>/dev/null \ + | openssl dgst -sha256 2>/dev/null \ + | awk '{print tolower($NF)}') + if [[ -z "$actual" || "$actual" != "$normalized" ]]; then + fail "Pulse server certificate fingerprint mismatch. Expected ${normalized}, got ${actual:-unavailable}." "$EXIT_PREFLIGHT_FAILED" + fi + + SERVER_FINGERPRINT="$normalized" + # curl must accept the self-signed chain after the explicit pin check. The + # downloaded installer and binary still require their own signatures. + INSECURE="true" + log_info "Pulse server certificate fingerprint verified." +} + build_exec_arg_items() { local include_token="${1:-true}" @@ -1370,6 +1422,7 @@ build_exec_arg_items() { if [[ "$ENABLE_PROXMOX" == "true" ]]; then EXEC_ARG_ITEMS+=(--enable-proxmox); fi if [[ -n "$PROXMOX_TYPE" ]]; then EXEC_ARG_ITEMS+=(--proxmox-type "$PROXMOX_TYPE"); fi if [[ "$INSECURE" == "true" ]]; then EXEC_ARG_ITEMS+=(--insecure); fi + if [[ -n "$SERVER_FINGERPRINT" ]]; then EXEC_ARG_ITEMS+=(--server-fingerprint "$SERVER_FINGERPRINT"); fi if [[ "$ENABLE_COMMANDS" == "true" ]]; then EXEC_ARG_ITEMS+=(--enable-commands); fi if [[ "$HEALTH_ADDR_SET" == "true" ]]; then EXEC_ARG_ITEMS+=(--health-addr "$HEALTH_ADDR"); fi if [[ "$ENROLL" == "true" ]]; then EXEC_ARG_ITEMS+=(--enroll); fi @@ -1568,6 +1621,9 @@ recover_connection_state() { INSECURE="true" fi fi + if [[ -z "$SERVER_FINGERPRINT" ]]; then + SERVER_FINGERPRINT=$(read_connection_state_value "$file" "PULSE_SERVER_FINGERPRINT") + fi if [[ -z "$CURL_CA_BUNDLE" ]]; then CURL_CA_BUNDLE=$(read_connection_state_value "$file" "PULSE_CACERT") fi @@ -1630,6 +1686,10 @@ apply_recovered_agent_arg_value() { if [[ -z "$CURL_CA_BUNDLE" ]]; then CURL_CA_BUNDLE="$value"; fi RECOVERED_AGENT_ARG_STATE="true" ;; + server-fingerprint) + if [[ -z "$SERVER_FINGERPRINT" ]]; then SERVER_FINGERPRINT="$value"; fi + RECOVERED_AGENT_ARG_STATE="true" + ;; health-addr) if [[ "$HEALTH_ADDR_SET" != "true" ]]; then HEALTH_ADDR="$value" @@ -1665,7 +1725,7 @@ recovered_connection_state_ready() { } update_connection_state_incomplete() { - [[ -z "$PULSE_URL" || -z "$PULSE_TOKEN" || -z "$AGENT_ID" || -z "$HOSTNAME_OVERRIDE" || -z "$CURL_CA_BUNDLE" || "$INSECURE" != "true" ]] + [[ -z "$PULSE_URL" || -z "$PULSE_TOKEN" || -z "$AGENT_ID" || -z "$HOSTNAME_OVERRIDE" || -z "$CURL_CA_BUNDLE" || -z "$SERVER_FINGERPRINT" || "$INSECURE" != "true" ]] } recover_connection_state_from_arg_stream() { @@ -1684,10 +1744,10 @@ recover_connection_state_from_arg_stream() { fi case "$arg" in - --url|--pulse-url|--token|--token-file|--interval|--agent-id|--hostname|--cacert|--health-addr|--state-dir|--kubeconfig|--proxmox-type|--disk-exclude|-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-cacert|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude) + --url|--pulse-url|--token|--token-file|--interval|--agent-id|--hostname|--cacert|--server-fingerprint|--health-addr|--state-dir|--kubeconfig|--proxmox-type|--disk-exclude|-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-cacert|-server-fingerprint|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude) pending_key=$(normalize_recovered_agent_arg_key "$arg") ;; - --url=*|--pulse-url=*|--token=*|--token-file=*|--interval=*|--agent-id=*|--hostname=*|--cacert=*|--health-addr=*|--state-dir=*|--kubeconfig=*|--proxmox-type=*|--disk-exclude=*|-url=*|-pulse-url=*|-token=*|-token-file=*|-interval=*|-agent-id=*|-hostname=*|-cacert=*|-health-addr=*|-state-dir=*|-kubeconfig=*|-proxmox-type=*|-disk-exclude=*) + --url=*|--pulse-url=*|--token=*|--token-file=*|--interval=*|--agent-id=*|--hostname=*|--cacert=*|--server-fingerprint=*|--health-addr=*|--state-dir=*|--kubeconfig=*|--proxmox-type=*|--disk-exclude=*|-url=*|-pulse-url=*|-token=*|-token-file=*|-interval=*|-agent-id=*|-hostname=*|-cacert=*|-server-fingerprint=*|-health-addr=*|-state-dir=*|-kubeconfig=*|-proxmox-type=*|-disk-exclude=*) key="${arg%%=*}" value="${arg#*=}" apply_recovered_agent_arg_value "$key" "$value" @@ -1816,6 +1876,11 @@ recover_connection_state_from_env_stream() { if [[ -z "$CURL_CA_BUNDLE" ]]; then CURL_CA_BUNDLE="$value"; fi RECOVERED_AGENT_ENV_STATE="true" ;; + PULSE_SERVER_FINGERPRINT=*) + value="${env_line#*=}" + if [[ -z "$SERVER_FINGERPRINT" ]]; then SERVER_FINGERPRINT="$value"; fi + RECOVERED_AGENT_ENV_STATE="true" + ;; esac done @@ -2140,6 +2205,7 @@ save_connection_info() { if [[ "$INSECURE" == "true" ]]; then write_connection_state_value "$conn_env" "PULSE_INSECURE_SKIP_VERIFY" "true" fi + write_connection_state_value "$conn_env" "PULSE_SERVER_FINGERPRINT" "$SERVER_FINGERPRINT" write_connection_state_value "$conn_env" "PULSE_CACERT" "$CURL_CA_BUNDLE" chmod 600 "$conn_env" # Save a copy of this install script for offline uninstall. @@ -2193,6 +2259,7 @@ while [[ $# -gt 0 ]]; do --proxmox-type) PROXMOX_TYPE="$2"; shift 2 ;; --insecure) INSECURE="true"; shift ;; --cacert) CURL_CA_BUNDLE="$2"; shift 2 ;; + --server-fingerprint) SERVER_FINGERPRINT="$2"; shift 2 ;; --enable-commands) ENABLE_COMMANDS="true"; shift ;; --health-addr) HEALTH_ADDR="$2"; HEALTH_ADDR_SET="true"; shift 2 ;; --enroll) ENROLL="true"; shift ;; @@ -2651,6 +2718,7 @@ if [[ ! "$url_lower" =~ ^https?:// ]]; then fi auto_enable_insecure_for_plain_http_url +verify_pinned_server_certificate # Validate token format when present (should be hex string, typically 64 chars) if [[ -n "$PULSE_TOKEN" && ! "$PULSE_TOKEN" =~ ^[a-fA-F0-9]+$ ]]; then @@ -2845,11 +2913,11 @@ if [[ ! -s "$TMP_BIN" ]]; then fail "Downloaded file is empty." "$EXIT_DOWNLOAD_FAILED" fi -# Check if it's a valid executable (ELF for Linux, Mach-O for macOS) -if [[ "$OS" == "linux" ]]; then +# Check if it's a valid executable (ELF for Linux/FreeBSD, Mach-O for macOS) +if [[ "$OS" == "linux" || "$OS" == "freebsd" ]]; then MAGIC=$(od -An -tx1 -N4 "$TMP_BIN" 2>/dev/null | tr -d ' \n' || true) if [[ "$MAGIC" != "7f454c46" ]]; then - fail "Downloaded file is not a valid Linux executable." "$EXIT_DOWNLOAD_FAILED" + fail "Downloaded file is not a valid ${OS} ELF executable." "$EXIT_DOWNLOAD_FAILED" fi elif [[ "$OS" == "darwin" ]]; then # Mach-O magic: feedface (32-bit) or feedfacf (64-bit) or cafebabe (universal) @@ -3323,6 +3391,7 @@ PULSE_ENABLE_DOCKER=${ENABLE_DOCKER} PULSE_ENABLE_KUBERNETES=${ENABLE_KUBERNETES} PULSE_KUBE_INCLUDE_ALL_PODS=${KUBE_INCLUDE_ALL_PODS} PULSE_KUBE_INCLUDE_ALL_DEPLOYMENTS=${KUBE_INCLUDE_ALL_DEPLOYMENTS} +PULSE_SERVER_FINGERPRINT=${SERVER_FINGERPRINT} EOF chmod 600 "$TRUENAS_ENV_FILE" diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index e511a40fe1..3475f9cabc 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -614,7 +614,7 @@ func TestDockerAndDemoBuildsUseCanonicalReleaseLdflags(t *testing.T) { dockerfile := string(dockerfileBytes) dockerRequired := []string{ `FROM --platform=linux/amd64 node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS frontend-builder`, - `FROM --platform=linux/amd64 golang:1.25.11-alpine@sha256:8d95af53d0d58e1759ddb4028285d9b1239067e4fbf4f544618cad0f60fbc354 AS backend-builder`, + `FROM --platform=linux/amd64 golang:1.26.5-alpine@sha256:0178a641fbb4858c5f1b48e34bdaabe0350a330a1b1149aabd498d0699ff5fb2 AS backend-builder`, `FROM backend-builder AS release-assets-builder`, `FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS agent_runtime`, `FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc AS pulse-runtime-base`, @@ -656,7 +656,7 @@ func TestDockerAndDemoBuildsUseCanonicalReleaseLdflags(t *testing.T) { t.Fatalf("hosted_runtime target must not depend on installer rendering or embedded agent artifacts:\n%s", hostedStage) } if strings.Contains(dockerfile, `FROM --platform=linux/amd64 node:20-alpine AS frontend-builder`) || - strings.Contains(dockerfile, `FROM --platform=linux/amd64 golang:1.25.11-alpine AS backend-builder`) || + strings.Contains(dockerfile, `FROM --platform=linux/amd64 golang:1.26.5-alpine AS backend-builder`) || strings.Contains(dockerfile, `FROM alpine:3.20 AS agent_runtime`) || strings.Contains(dockerfile, `FROM alpine:3.20 AS pulse-runtime-base`) { t.Fatal("Dockerfile base images must be pinned by immutable @sha256 digests") @@ -714,6 +714,29 @@ func TestAgentRuntimeImagePersistsAgentIdentityByDefault(t *testing.T) { } } +func TestReleaseCandidateRequiresPlatformNativeAgentSigning(t *testing.T) { + assertFileContainsAll(t, repoFile(".github", "workflows", "build-release-candidate.yml"), + `require_platform_signing:`, + `sign-macos-agent:`, + `codesign --force --timestamp --options runtime`, + `xcrun notarytool submit`, + `spctl --assess --type execute`, + `sign-windows-agent:`, + `signtool sign`, + `signtool verify /pa /v`, + `PULSE_AGENT_NATIVE_BINARIES_DIR:`, + ) + assertFileContainsAll(t, repoFile(".github", "workflows", "create-release.yml"), + `require_platform_signing: true`, + ) + assertFileContainsAll(t, repoFile("scripts", "build-release.sh"), + `PULSE_AGENT_NATIVE_BINARIES_DIR`, + `native_targets=(darwin-amd64 darwin-arm64 windows-amd64 windows-arm64 windows-386)`, + `Applied platform-native signed Unified Agent binaries.`, + `platform signing is required but PULSE_AGENT_NATIVE_BINARIES_DIR is empty.`, + ) +} + func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) { createReleaseBytes, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml")) if err != nil { diff --git a/scripts/installtests/install_ps1_test.go b/scripts/installtests/install_ps1_test.go index 7c194698e5..89e28bedec 100644 --- a/scripts/installtests/install_ps1_test.go +++ b/scripts/installtests/install_ps1_test.go @@ -46,6 +46,27 @@ func TestInstallPS1DockerModeDefaultsHostOff(t *testing.T) { } } +func TestInstallPS1PersistsAndVerifiesServerFingerprint(t *testing.T) { + content, err := os.ReadFile(repoFile("scripts", "install.ps1")) + if err != nil { + t.Fatalf("read install.ps1: %v", err) + } + + script := string(content) + required := []string{ + `[string]$ServerFingerprint = $env:PULSE_SERVER_FINGERPRINT,`, + `$sha256.ComputeHash($certificate.GetRawCertData())`, + `$lines += "PULSE_SERVER_FINGERPRINT='$ServerFingerprint'"`, + `$ServerFingerprint = Get-ConnectionStateValue "PULSE_SERVER_FINGERPRINT"`, + `$ServiceArgs += @("--server-fingerprint", "` + "`" + `"$ServerFingerprint` + "`" + `"")`, + } + for _, needle := range required { + if !strings.Contains(script, needle) { + t.Fatalf("install.ps1 missing server-fingerprint lifecycle contract: %s", needle) + } + } +} + func TestInstallPS1AllowsMissingTokenForOptionalAuth(t *testing.T) { content, err := os.ReadFile(repoFile("scripts", "install.ps1")) if err != nil { @@ -164,7 +185,7 @@ func TestInstallPS1UsesInsecureTlsForRuntimeTransport(t *testing.T) { script := string(content) required := []string{ `function Invoke-WithOptionalInsecureTls {`, - `if ($AllowInsecure -or $null -ne $CustomCaCertificate) {`, + `if ($AllowInsecure -or $null -ne $CustomCaCertificate -or -not [string]::IsNullOrWhiteSpace($ServerFingerprint)) {`, `if ($AllowInsecure) {`, `return Test-CertificateTrustedByCustomCa -Certificate $certificate -CustomCaCertificate $CustomCaCertificate`, `if ($Url.ToLowerInvariant().StartsWith("http://") -and -not $Insecure) {`, diff --git a/scripts/installtests/install_sh_test.go b/scripts/installtests/install_sh_test.go index 20e69a989c..069cb71af4 100644 --- a/scripts/installtests/install_sh_test.go +++ b/scripts/installtests/install_sh_test.go @@ -41,6 +41,30 @@ func TestInstallSHAllowsMissingTokenForOptionalAuth(t *testing.T) { } } +func TestInstallSHPersistsAndVerifiesServerFingerprint(t *testing.T) { + content, err := os.ReadFile(repoFile("scripts", "install.sh")) + if err != nil { + t.Fatalf("read install.sh: %v", err) + } + + script := string(content) + required := []string{ + `SERVER_FINGERPRINT="${PULSE_SERVER_FINGERPRINT:-}"`, + `--server-fingerprint `, + `verify_pinned_server_certificate() {`, + `openssl s_client -connect "$target" -servername "$host"`, + `EXEC_ARG_ITEMS+=(--server-fingerprint "$SERVER_FINGERPRINT")`, + `write_connection_state_value "$conn_env" "PULSE_SERVER_FINGERPRINT" "$SERVER_FINGERPRINT"`, + `SERVER_FINGERPRINT=$(read_connection_state_value "$file" "PULSE_SERVER_FINGERPRINT")`, + `if [[ "$OS" == "linux" || "$OS" == "freebsd" ]]; then`, + } + for _, needle := range required { + if !strings.Contains(script, needle) { + t.Fatalf("install.sh missing server-fingerprint lifecycle contract: %s", needle) + } + } +} + func TestInstallSHAutoDetectProxmoxKeepsRuntimeTypeUnpinned(t *testing.T) { content, err := os.ReadFile(repoFile("scripts", "install.sh")) if err != nil { @@ -687,7 +711,7 @@ func TestInstallSHSupportsSavedStateUpdateMode(t *testing.T) { `recover_connection_state_from_arg_stream`, `recover_token_from_default_agent_token_file() {`, `normalize_recovered_agent_arg_key() {`, - `-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-cacert|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude)`, + `-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-cacert|-server-fingerprint|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude)`, `--enable-host|-enable-host|--enable-host=true|-enable-host=true)`, `recover_connection_state_from_env_stream`, `recovered_connection_state_ready() {`, diff --git a/scripts/installtests/provider_msp_deploy_test.go b/scripts/installtests/provider_msp_deploy_test.go index a94805c5bf..b53c615d20 100644 --- a/scripts/installtests/provider_msp_deploy_test.go +++ b/scripts/installtests/provider_msp_deploy_test.go @@ -269,7 +269,7 @@ func TestProviderMSPControlPlaneDockerfileBuildsReleaseLicenseBinary(t *testing. "FROM --platform=linux/amd64 node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS frontend-builder", "npm ci", "npm run build", - "FROM --platform=$BUILDPLATFORM golang:1.25.11-alpine@sha256:8d95af53d0d58e1759ddb4028285d9b1239067e4fbf4f544618cad0f60fbc354 AS builder", + "FROM --platform=$BUILDPLATFORM golang:1.26.5-alpine@sha256:0178a641fbb4858c5f1b48e34bdaabe0350a330a1b1149aabd498d0699ff5fb2 AS builder", "FROM alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc", "ARG PULSE_LICENSE_PUBLIC_KEY_SHA256", "ARG TARGETOS", diff --git a/scripts/tests/test-hot-dev-runtime.sh b/scripts/tests/test-hot-dev-runtime.sh index ca9ff4d820..0f668fba47 100755 --- a/scripts/tests/test-hot-dev-runtime.sh +++ b/scripts/tests/test-hot-dev-runtime.sh @@ -319,8 +319,8 @@ test_go_module_security_dependency_floors() { output="$(cd "${ROOT_DIR}" && go list -m golang.org/x/net golang.org/x/crypto golang.org/x/sys)" assert_contains "Go module floor keeps x/net past restricted-outbound advisories" "${output}" "golang.org/x/net v0.56.0" - assert_contains "Go module floor keeps x/crypto aligned with x/net security floor" "${output}" "golang.org/x/crypto v0.53.0" - assert_contains "Go module floor keeps x/sys aligned with security module graph" "${output}" "golang.org/x/sys v0.46.0" + assert_contains "Go module floor keeps x/crypto aligned with x/net security floor" "${output}" "golang.org/x/crypto v0.54.0" + assert_contains "Go module floor keeps x/sys aligned with security module graph" "${output}" "golang.org/x/sys v0.47.0" } source "${HOT_DEV_RUNTIME_LIB}"