mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
a0170d3252
The in-app updater validated download URL and channel but never compared the target against the running version, so any valid older release asset URL installed silently while the UI presented it as an update. ApplyUpdate now rejects targets at or below the running version on both the community and Pro broker paths before any history entry or download, with an explicit allowDowngrade opt-in on POST /api/updates/apply for sanctioned cases. The rollback half already existed but nothing reached it: createBackup retains three backups, restoreBackup works, and history records BackupPath, yet no endpoint or UI called restoreBackup. RollbackToBackup restores the retained backup recorded on a history entry after re-validating the path against the managed backup roots, shares the update-in-flight slot with ApplyUpdate, records an Action rollback history entry linked to the source update, marks that update rolled_back, streams a restoring stage through the existing status/SSE machinery, and restarts via the exit-for-systemd path. POST /api/updates/rollback carries it with the same RequireAdmin plus settings:write gating as apply. Rollback is purely local, so the Pro edition gate never applies to it. Settings now has the update history surface that was missing entirely: nothing called /api/updates/history before. The Updates panel lists recent updates with a Roll back action on successful entries whose backup is still retained, behind a confirmation dialog naming the restore version, and the rollback rides updateStore's shared pending-apply marker for the post-restart toast. restoreBackup also honors PULSE_INSTALL_DIR now instead of hardcoding /opt/pulse, matching createBackup. Contract deltas ride along: api-contracts picks up the rollback transport and downgrade-conflict semantics, agent-lifecycle and storage-recovery pin rollback as server self-update plumbing, ai-runtime and cloud-paid pin the update watcher stage vocabulary as non-assistant non-paid shell chrome, and frontend-primitives adds UpdateHistorySection as the history/rollback presentation owner with matching architecture proofs.
691 lines
19 KiB
Go
691 lines
19 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRouterRouteInventory(t *testing.T) {
|
|
literalRoutes, dynamicRoutes, bareLiteralRoutes := parseRouterRoutes(t)
|
|
|
|
expectedAll := sliceToSet(t, allRouteAllowlist, "all route allowlist")
|
|
expectedPublic := sliceToSet(t, publicRouteAllowlist, "public route allowlist")
|
|
expectedDynamic := sliceToSet(t, dynamicRouteAllowlist, "dynamic route allowlist")
|
|
expectedBare := sliceToSet(t, bareRouteAllowlist, "bare route allowlist")
|
|
|
|
actualAll := sliceToSet(t, literalRoutes, "router routes")
|
|
actualDynamic := sliceToSet(t, dynamicRoutes, "router dynamic routes")
|
|
actualBare := sliceToSet(t, bareLiteralRoutes, "router bare routes")
|
|
|
|
if missing := setDifference(actualAll, expectedAll); len(missing) > 0 {
|
|
t.Fatalf("routes missing from allowlist: %s", strings.Join(sortedKeys(missing), ", "))
|
|
}
|
|
if stale := setDifference(expectedAll, actualAll); len(stale) > 0 {
|
|
t.Fatalf("allowlist contains routes not in router.go: %s", strings.Join(sortedKeys(stale), ", "))
|
|
}
|
|
|
|
if missing := setDifference(actualDynamic, expectedDynamic); len(missing) > 0 {
|
|
t.Fatalf("dynamic routes missing from allowlist: %s", strings.Join(sortedKeys(missing), ", "))
|
|
}
|
|
if stale := setDifference(expectedDynamic, actualDynamic); len(stale) > 0 {
|
|
t.Fatalf("dynamic allowlist contains routes not in router.go: %s", strings.Join(sortedKeys(stale), ", "))
|
|
}
|
|
|
|
if missing := setDifference(expectedPublic, expectedAll); len(missing) > 0 {
|
|
t.Fatalf("public routes missing from full allowlist: %s", strings.Join(sortedKeys(missing), ", "))
|
|
}
|
|
if missing := setDifference(actualBare, expectedBare); len(missing) > 0 {
|
|
t.Fatalf("bare routes missing from allowlist: %s", strings.Join(sortedKeys(missing), ", "))
|
|
}
|
|
if stale := setDifference(expectedBare, actualBare); len(stale) > 0 {
|
|
t.Fatalf("bare allowlist contains routes not registered bare in router.go: %s", strings.Join(sortedKeys(stale), ", "))
|
|
}
|
|
if missing := setDifference(expectedPublic, expectedBare); len(missing) > 0 {
|
|
t.Fatalf("public routes must be registered bare: %s", strings.Join(sortedKeys(missing), ", "))
|
|
}
|
|
}
|
|
|
|
func TestRouterRouteInventory_VMwarePhase1ExclusionIntegrity(t *testing.T) {
|
|
literalRoutes, _, _ := parseRouterRoutes(t)
|
|
actual := sliceToSet(t, literalRoutes, "router routes")
|
|
|
|
forbidden := []string{
|
|
"/api/vmware/hosts",
|
|
"/api/vmware/vms",
|
|
"/api/vmware/datastores",
|
|
"/api/vmware/events",
|
|
"/api/vmware/tasks",
|
|
"/api/vmware/alarms",
|
|
}
|
|
for _, route := range forbidden {
|
|
if _, ok := actual[route]; ok {
|
|
t.Fatalf("vmware phase-1 exclusion failed: unexpected route %q is registered", route)
|
|
}
|
|
}
|
|
|
|
var vmwareRoutes []string
|
|
for route := range actual {
|
|
if strings.HasPrefix(route, "/api/vmware/") {
|
|
vmwareRoutes = append(vmwareRoutes, route)
|
|
}
|
|
}
|
|
sort.Strings(vmwareRoutes)
|
|
|
|
expected := []string{
|
|
"/api/vmware/connections",
|
|
"/api/vmware/connections/",
|
|
"/api/vmware/connections/preview",
|
|
"/api/vmware/connections/test",
|
|
}
|
|
if strings.Join(vmwareRoutes, ",") != strings.Join(expected, ",") {
|
|
t.Fatalf("vmware phase-1 route family drifted: got %v, want %v", vmwareRoutes, expected)
|
|
}
|
|
}
|
|
|
|
func parseRouterRoutes(t *testing.T) ([]string, []string, []string) {
|
|
t.Helper()
|
|
|
|
_, file, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatalf("failed to locate test file path")
|
|
}
|
|
routerFiles := []string{
|
|
"router.go",
|
|
"router_routes_registration.go",
|
|
"router_routes_ai_relay.go",
|
|
"router_routes_monitoring.go",
|
|
"router_routes_auth_security.go",
|
|
"router_routes_licensing.go",
|
|
"router_routes_cloud.go",
|
|
"router_routes_debug.go",
|
|
}
|
|
|
|
fset := token.NewFileSet()
|
|
|
|
var literalRoutes []string
|
|
var dynamicRoutes []string
|
|
var bareLiteralRoutes []string
|
|
var found bool
|
|
|
|
for _, routerFile := range routerFiles {
|
|
routerPath := filepath.Join(filepath.Dir(file), routerFile)
|
|
fileAST, err := parser.ParseFile(fset, routerPath, nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse %s: %v", routerFile, err)
|
|
}
|
|
|
|
ast.Inspect(fileAST, func(node ast.Node) bool {
|
|
call, ok := node.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
selector, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok || selector.Sel == nil {
|
|
return true
|
|
}
|
|
if selector.Sel.Name != "Handle" && selector.Sel.Name != "HandleFunc" {
|
|
return true
|
|
}
|
|
if len(call.Args) < 2 {
|
|
return true
|
|
}
|
|
route, isDynamic := routeLiteral(call.Args[0])
|
|
if route == "" {
|
|
return true
|
|
}
|
|
found = true
|
|
if isDynamic {
|
|
dynamicRoutes = append(dynamicRoutes, route)
|
|
} else {
|
|
literalRoutes = append(literalRoutes, route)
|
|
if !isProtectedHandler(call.Args[1]) {
|
|
bareLiteralRoutes = append(bareLiteralRoutes, route)
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
if !found {
|
|
t.Fatalf("no routes found in parsed router files")
|
|
}
|
|
|
|
return literalRoutes, dynamicRoutes, bareLiteralRoutes
|
|
}
|
|
|
|
func routeLiteral(expr ast.Expr) (string, bool) {
|
|
switch v := expr.(type) {
|
|
case *ast.BasicLit:
|
|
if v.Kind != token.STRING {
|
|
return "", true
|
|
}
|
|
unquoted, err := strconv.Unquote(v.Value)
|
|
if err != nil {
|
|
unquoted = strings.Trim(v.Value, "`\"'")
|
|
}
|
|
return unquoted, false
|
|
case *ast.Ident:
|
|
return v.Name, true
|
|
case *ast.SelectorExpr:
|
|
return fmt.Sprintf("%s.%s", selectorName(v.X), v.Sel.Name), true
|
|
default:
|
|
return fmt.Sprintf("%T", expr), true
|
|
}
|
|
}
|
|
|
|
func selectorName(expr ast.Expr) string {
|
|
switch v := expr.(type) {
|
|
case *ast.Ident:
|
|
return v.Name
|
|
case *ast.SelectorExpr:
|
|
return selectorName(v.X) + "." + v.Sel.Name
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func isProtectedHandler(expr ast.Expr) bool {
|
|
call, ok := expr.(*ast.CallExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
switch fn := call.Fun.(type) {
|
|
case *ast.Ident:
|
|
if isAuthWrapper(fn.Name) {
|
|
return true
|
|
}
|
|
case *ast.SelectorExpr:
|
|
if isAuthWrapper(fn.Sel.Name) {
|
|
return true
|
|
}
|
|
}
|
|
for _, arg := range call.Args {
|
|
if isProtectedHandler(arg) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isAuthWrapper(name string) bool {
|
|
switch name {
|
|
case "RequireAuth", "RequireAdmin", "RequirePermission", "RequireOrgOwnerOrPlatformAdmin", "RequirePlatformAdmin":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func sliceToSet(t *testing.T, items []string, name string) map[string]struct{} {
|
|
t.Helper()
|
|
set := make(map[string]struct{}, len(items))
|
|
for _, item := range items {
|
|
if _, exists := set[item]; exists {
|
|
t.Fatalf("duplicate entry %q in %s", item, name)
|
|
}
|
|
set[item] = struct{}{}
|
|
}
|
|
return set
|
|
}
|
|
|
|
func setDifference(a, b map[string]struct{}) map[string]struct{} {
|
|
diff := make(map[string]struct{})
|
|
for key := range a {
|
|
if _, ok := b[key]; !ok {
|
|
diff[key] = struct{}{}
|
|
}
|
|
}
|
|
return diff
|
|
}
|
|
|
|
func sortedKeys(set map[string]struct{}) []string {
|
|
keys := make([]string, 0, len(set))
|
|
for key := range set {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
var dynamicRouteAllowlist = []string{
|
|
"agentcapabilities.OperationsLoopStatusCompatibilityPath",
|
|
"agentcapabilities.PatrolControlStatusCapabilityPath",
|
|
}
|
|
|
|
var publicRouteAllowlist = []string{
|
|
"/api/health",
|
|
"/api/version",
|
|
"/api/agent/capabilities",
|
|
"/api/agent/version",
|
|
"/api/agent/ws",
|
|
"/api/server/info",
|
|
"/api/security/status",
|
|
"/api/security/validate-bootstrap-token",
|
|
"/api/security/quick-setup",
|
|
"/api/security/recovery",
|
|
"/api/login",
|
|
"/api/public/signup",
|
|
"/api/public/magic-link/request",
|
|
"/api/public/magic-link/verify",
|
|
"/api/cloud/handoff/exchange",
|
|
"/api/ai/oauth/callback",
|
|
"/api/webhooks/stripe",
|
|
"/api/setup-script",
|
|
"/api/config/export",
|
|
"/api/config/import",
|
|
"/api/system/verify-temperature-ssh",
|
|
"/api/system/ssh-config",
|
|
"/api/auto-register",
|
|
"/api/auto-unregister",
|
|
"/auth/cloud-handoff",
|
|
licensePurchaseActivationPath,
|
|
"/install.sh",
|
|
"/install.ps1",
|
|
"/download/pulse-agent",
|
|
}
|
|
|
|
var bareRouteAllowlist = []string{
|
|
"/api/agent-deploy/jobs/",
|
|
"/api/agent-deploy/preflights/",
|
|
"/api/agent/capabilities",
|
|
"/api/agent/version",
|
|
"/api/agent/ws",
|
|
"/api/ai/oauth/callback",
|
|
"/api/auto-register",
|
|
"/api/auto-unregister",
|
|
"/api/webhooks/stripe",
|
|
"/api/config/export",
|
|
"/api/config/import",
|
|
"/api/config/nodes",
|
|
"/api/config/nodes/",
|
|
"/api/config/nodes/test-config",
|
|
"/api/config/nodes/test-connection",
|
|
"/api/config/system",
|
|
"/api/connections",
|
|
"/api/connections/probe",
|
|
"/api/availability-targets",
|
|
"/api/availability-targets/test",
|
|
"/api/availability-targets/",
|
|
"/api/truenas/connections",
|
|
"/api/truenas/connections/preview",
|
|
"/api/truenas/connections/test",
|
|
"/api/truenas/connections/",
|
|
"/api/vmware/connections",
|
|
"/api/vmware/connections/preview",
|
|
"/api/vmware/connections/test",
|
|
"/api/vmware/connections/",
|
|
"/api/health",
|
|
"/api/login",
|
|
"/api/logout",
|
|
"/api/logs/level",
|
|
"/api/oidc/",
|
|
"/api/security/apply-restart",
|
|
"/api/security/change-password",
|
|
"/api/security/dev/reset-first-run",
|
|
"/api/security/quick-setup",
|
|
"/api/security/recovery",
|
|
"/api/security/regenerate-token",
|
|
"/api/security/reset-lockout",
|
|
"/api/security/status",
|
|
"/api/security/validate-bootstrap-token",
|
|
"/api/security/validate-token",
|
|
"/api/server/info",
|
|
"/api/setup-script",
|
|
"/api/state",
|
|
"/api/state/summary",
|
|
"/api/system/mock-mode",
|
|
"/api/system/ssh-config",
|
|
"/api/system/verify-temperature-ssh",
|
|
"/api/version",
|
|
"/download/pulse-agent",
|
|
"/install.ps1",
|
|
"/install.sh",
|
|
"/api/public/signup",
|
|
"/api/public/magic-link/request",
|
|
"/api/public/magic-link/verify",
|
|
"/api/cloud/handoff/exchange",
|
|
"/api/clusters/",
|
|
"/auth/cloud-handoff",
|
|
licensePurchaseActivationPath,
|
|
"/ws",
|
|
"/api/saml/",
|
|
}
|
|
|
|
var allRouteAllowlist = []string{
|
|
"/debug/pprof",
|
|
"/debug/pprof/",
|
|
"/debug/pprof/cmdline",
|
|
"/debug/pprof/profile",
|
|
"/debug/pprof/symbol",
|
|
"/debug/pprof/trace",
|
|
"/api/health",
|
|
"/api/monitoring/scheduler/health",
|
|
"/api/state",
|
|
"/api/state/summary",
|
|
"/api/logs/stream",
|
|
"/api/logs/download",
|
|
"/api/logs/level",
|
|
"/api/agents/docker/report",
|
|
"/api/agents/kubernetes/report",
|
|
"/api/agents/agent/report",
|
|
"/api/agents/host/report",
|
|
"/api/agents/agent/lookup",
|
|
"/api/agents/host/lookup",
|
|
"/api/agents/agent/uninstall",
|
|
"/api/agents/host/uninstall",
|
|
"/api/agents/agent/unlink",
|
|
"/api/agents/host/unlink",
|
|
"/api/agents/agent/link",
|
|
"/api/agents/host/link",
|
|
"/api/agents/diagnostics",
|
|
"/api/agents/agent/",
|
|
"/api/agents/host/",
|
|
"/api/agents/docker/commands/",
|
|
"/api/agents/docker/runtimes/",
|
|
"/api/agents/docker/containers/update",
|
|
"/api/agents/kubernetes/clusters/",
|
|
"/api/version",
|
|
"/api/storage/",
|
|
"/api/storage-charts",
|
|
"/api/charts",
|
|
"/api/charts/workloads",
|
|
"/api/charts/infrastructure",
|
|
"/api/charts/storage-summary",
|
|
"/api/charts/workloads-summary",
|
|
"/api/metrics-store/stats",
|
|
"/api/metrics-store/history",
|
|
"/api/diagnostics",
|
|
"/api/diagnostics/docker/prepare-token",
|
|
"/api/config",
|
|
"/api/recovery/points",
|
|
"/api/recovery/series",
|
|
"/api/recovery/facets",
|
|
"/api/recovery/rollups",
|
|
"/api/resources",
|
|
"/api/resources/storage-incidents",
|
|
"/api/resources/storage-summary",
|
|
"/api/resources/k8s/namespaces",
|
|
"/api/resources/stats",
|
|
"/api/resources/timeline",
|
|
"/api/resources/",
|
|
"/api/resources/{id}/facets",
|
|
"/api/resources/{id}/timeline",
|
|
"/api/resources/{id}/operator-state",
|
|
"/api/resources/{id}/maintenance-verifications",
|
|
"POST /api/resources/{id}/maintenance-verifications/rerun",
|
|
"/api/maintenance-verifications/{reportId}",
|
|
"POST /api/maintenance-verifications/{reportId}/review",
|
|
"/api/agent/resource-context/{id}",
|
|
"/api/agent/resource-capabilities/{id}",
|
|
"/api/agent/fleet-context",
|
|
"/api/agent/capabilities",
|
|
"/api/agent/events",
|
|
"POST /api/actions/plan",
|
|
"POST /api/actions/{id}/decision",
|
|
"POST /api/actions/{id}/execute",
|
|
"/api/guests/metadata",
|
|
"/api/guests/metadata/",
|
|
"/api/docker/metadata",
|
|
"/api/docker/metadata/",
|
|
"/api/docker/runtimes/metadata",
|
|
"/api/docker/runtimes/metadata/",
|
|
"/api/agents/metadata",
|
|
"/api/agents/metadata/",
|
|
"GET /api/settings/relay",
|
|
"GET /api/settings/relay/status",
|
|
"PUT /api/settings/relay",
|
|
"GET /api/onboarding/qr",
|
|
"POST /api/onboarding/validate",
|
|
"GET /api/onboarding/deep-link",
|
|
"/api/updates/check",
|
|
"/api/updates/apply",
|
|
"/api/updates/rollback",
|
|
"/api/updates/status",
|
|
"/api/updates/stream",
|
|
"/api/updates/plan",
|
|
"/api/updates/history",
|
|
"/api/updates/history/entry",
|
|
"/api/infra-updates",
|
|
"/api/infra-updates/summary",
|
|
"/api/infra-updates/check",
|
|
"/api/infra-updates/agent/",
|
|
"/api/infra-updates/",
|
|
"/api/config/nodes",
|
|
"/api/security/validate-bootstrap-token",
|
|
"/api/config/nodes/test-config",
|
|
"/api/config/nodes/test-connection",
|
|
"/api/config/nodes/",
|
|
"/api/connections",
|
|
"/api/connections/probe",
|
|
"/api/availability-targets",
|
|
"/api/availability-targets/test",
|
|
"/api/availability-targets/",
|
|
"/api/truenas/connections",
|
|
"/api/truenas/connections/preview",
|
|
"/api/truenas/connections/test",
|
|
"/api/truenas/connections/",
|
|
"/api/vmware/connections",
|
|
"/api/vmware/connections/preview",
|
|
"/api/vmware/connections/test",
|
|
"/api/vmware/connections/",
|
|
"/api/admin/profiles/",
|
|
"/api/config/system",
|
|
"/api/system/settings/telemetry-preview",
|
|
"/api/system/settings/telemetry-reset-id",
|
|
"/api/system/mock-mode",
|
|
"/api/license/status",
|
|
"/api/webhooks/stripe",
|
|
"/api/license/features",
|
|
"/api/license/activate",
|
|
"/api/license/clear",
|
|
"GET /api/license/runtime-capabilities",
|
|
"GET /api/license/commercial-posture",
|
|
"GET /api/license/entitlements",
|
|
"GET /api/license/monitored-system-ledger",
|
|
"POST /api/license/monitored-system-ledger/explain",
|
|
"POST /api/license/monitored-system-ledger/preview",
|
|
"GET /auth/license-purchase-start",
|
|
"GET /api/orgs",
|
|
"POST /api/orgs",
|
|
"GET /api/orgs/{id}",
|
|
"PUT /api/orgs/{id}",
|
|
"DELETE /api/orgs/{id}",
|
|
"GET /api/orgs/{id}/members",
|
|
"POST /api/orgs/{id}/members",
|
|
"DELETE /api/orgs/{id}/members/{userId}",
|
|
"GET /api/orgs/{id}/invitations",
|
|
"DELETE /api/orgs/{id}/invitations/{userId}",
|
|
"GET /api/org-invitations",
|
|
"POST /api/org-invitations/{id}/accept",
|
|
"DELETE /api/org-invitations/{id}",
|
|
"GET /api/orgs/{id}/shares",
|
|
"GET /api/orgs/{id}/shares/incoming",
|
|
"POST /api/orgs/{id}/shares/incoming/{shareId}/accept",
|
|
"POST /api/orgs/{id}/shares",
|
|
"DELETE /api/orgs/{id}/shares/incoming/{shareId}",
|
|
"DELETE /api/orgs/{id}/shares/{shareId}",
|
|
"GET /api/audit",
|
|
"GET /api/audit/",
|
|
"GET /api/audit/{id}/verify",
|
|
"GET /api/audit/export",
|
|
"GET /api/audit/summary",
|
|
"GET /api/audit/actions",
|
|
"GET /api/audit/actions/{id}/events",
|
|
"GET /api/audit/exports",
|
|
"GET /api/admin/orgs/{id}/billing-state",
|
|
"PUT /api/admin/orgs/{id}/billing-state",
|
|
"POST /api/admin/orgs/{id}/suspend",
|
|
"POST /api/admin/orgs/{id}/unsuspend",
|
|
"POST /api/admin/orgs/{id}/soft-delete",
|
|
"GET /api/hosted/organizations",
|
|
"POST /api/admin/orgs/{id}/agent-install-command",
|
|
"/api/public/signup",
|
|
"/api/public/magic-link/request",
|
|
"/api/public/magic-link/verify",
|
|
"/api/cloud/handoff/exchange",
|
|
"/auth/cloud-handoff",
|
|
licensePurchaseActivationPath,
|
|
"/api/saml/",
|
|
"/api/admin/roles",
|
|
"/api/admin/roles/",
|
|
"/api/admin/users",
|
|
"/api/admin/users/",
|
|
"GET /api/admin/rbac/integrity",
|
|
"POST /api/admin/rbac/reset-admin",
|
|
"/api/admin/reports/generate",
|
|
"/api/admin/reports/generate-multi",
|
|
"/api/admin/reports/catalog",
|
|
"/api/admin/reports/inventory/vms/export",
|
|
"GET /api/admin/reports/schedules",
|
|
"POST /api/admin/reports/schedules",
|
|
"PUT /api/admin/reports/schedules/{id}",
|
|
"DELETE /api/admin/reports/schedules/{id}",
|
|
"POST /api/admin/reports/schedules/{id}/run",
|
|
"/api/admin/webhooks/audit",
|
|
"/api/security/change-password",
|
|
"/api/security/dev/reset-first-run",
|
|
"/api/logout",
|
|
"/api/login",
|
|
"/api/security/reset-lockout",
|
|
"/api/oidc/",
|
|
"/api/security/sso/providers/test",
|
|
"/api/security/sso/providers/metadata/preview",
|
|
"/api/security/sso/providers",
|
|
"/api/security/sso/providers/",
|
|
"/api/security/tokens/relay-mobile",
|
|
"/api/security/tokens",
|
|
"/api/security/tokens/",
|
|
"/api/security/status",
|
|
"/api/security/quick-setup",
|
|
"/api/security/regenerate-token",
|
|
"/api/security/validate-token",
|
|
"/api/security/apply-restart",
|
|
"/api/security/recovery",
|
|
"/api/config/export",
|
|
"/api/config/import",
|
|
"/api/setup-script",
|
|
"/api/setup-script-url",
|
|
"/api/agent-install-command",
|
|
"/api/auto-register",
|
|
"/api/auto-unregister",
|
|
"/api/discover",
|
|
"/api/test-notification",
|
|
"/api/alerts/",
|
|
"/api/notifications/",
|
|
"/api/notifications/dlq",
|
|
"/api/notifications/queue/stats",
|
|
"/api/notifications/dlq/retry",
|
|
"/api/notifications/dlq/delete",
|
|
"/api/system/settings",
|
|
"/api/system/settings/update",
|
|
"/api/system/ssh-config",
|
|
"/api/system/verify-temperature-ssh",
|
|
"/api/settings/ai",
|
|
"/api/settings/ai/update",
|
|
"/api/ai/test",
|
|
"/api/ai/test/{provider}",
|
|
"/api/ai/models",
|
|
"/api/ai/execute",
|
|
"/api/ai/execute/stream",
|
|
"/api/ai/kubernetes/analyze",
|
|
"/api/ai/investigate-alert",
|
|
"/api/ai/run-command",
|
|
"/api/ai/knowledge",
|
|
"/api/ai/knowledge/save",
|
|
"/api/ai/knowledge/delete",
|
|
"/api/ai/knowledge/export",
|
|
"/api/ai/knowledge/import",
|
|
"/api/ai/knowledge/clear",
|
|
"/api/ai/debug/context",
|
|
"/api/ai/agents",
|
|
"/api/ai/cost/summary",
|
|
"/api/ai/cost/reset",
|
|
"/api/ai/cost/export",
|
|
"/api/ai/oauth/start",
|
|
"/api/ai/oauth/exchange",
|
|
"/api/ai/oauth/callback",
|
|
"/api/ai/oauth/disconnect",
|
|
"/api/ai/patrol/status",
|
|
"/api/ai/patrol/stream",
|
|
"/api/ai/patrol/findings",
|
|
"/api/ai/patrol/history",
|
|
"/api/ai/patrol/run",
|
|
"/api/ai/patrol/preflight",
|
|
"/api/ai/patrol/acknowledge",
|
|
"/api/ai/patrol/dismiss",
|
|
"/api/ai/patrol/findings/note",
|
|
"/api/ai/patrol/suppress",
|
|
"/api/ai/patrol/snooze",
|
|
"/api/ai/patrol/resolve",
|
|
"/api/ai/patrol/runs",
|
|
"/api/ai/patrol/runs/",
|
|
"/api/ai/patrol/suppressions",
|
|
"/api/ai/patrol/suppressions/",
|
|
"/api/ai/patrol/dismissed",
|
|
"/api/ai/patrol/autonomy",
|
|
"/api/ai/findings/",
|
|
"/api/ai/intelligence",
|
|
"/api/ai/intelligence/patterns",
|
|
"/api/ai/intelligence/predictions",
|
|
"/api/ai/intelligence/correlations",
|
|
"/api/ai/intelligence/changes",
|
|
"/api/ai/intelligence/baselines",
|
|
"/api/ai/intelligence/remediations",
|
|
"/api/ai/intelligence/anomalies",
|
|
"/api/ai/intelligence/learning",
|
|
"/api/ai/unified/findings",
|
|
"/api/ai/forecast",
|
|
"/api/ai/forecasts/overview",
|
|
"/api/ai/learning/preferences",
|
|
"/api/ai/proxmox/events",
|
|
"/api/ai/proxmox/correlations",
|
|
"/api/ai/remediation/plans",
|
|
"/api/ai/remediation/plan",
|
|
"/api/ai/remediation/approve",
|
|
"/api/ai/remediation/execute",
|
|
"/api/ai/remediation/rollback",
|
|
"/api/ai/circuit/status",
|
|
"/api/ai/incidents",
|
|
"/api/ai/incidents/",
|
|
"/api/ai/status",
|
|
"/api/ai/assistant/surface-tools",
|
|
"/api/ai/workflow-prompts/render",
|
|
"/api/ai/workflow-prompts/activity",
|
|
"/api/ai/chat",
|
|
"/api/ai/sessions",
|
|
"/api/ai/sessions/",
|
|
"/api/ai/approvals",
|
|
"/api/ai/approvals/",
|
|
"/api/ai/question/",
|
|
"/api/discovery",
|
|
"/api/discovery/status",
|
|
"/api/discovery/run",
|
|
"/api/discovery/settings",
|
|
"/api/discovery/info/",
|
|
"/api/discovery/type/",
|
|
"/api/discovery/agent/",
|
|
"/api/discovery/",
|
|
"/api/agent/ws",
|
|
"POST /api/agent/workflow-prompt-activity",
|
|
"/api/agents/agent/enroll",
|
|
"/api/agents/host/enroll",
|
|
"/api/clusters/",
|
|
"/api/agent-deploy/preflights/",
|
|
"/api/agent-deploy/jobs/",
|
|
"/install.sh",
|
|
"/install.ps1",
|
|
"/download/pulse-agent",
|
|
"/api/agent/version",
|
|
"/api/server/info",
|
|
"/ws",
|
|
"/simple-stats",
|
|
}
|