mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
644 lines
17 KiB
Go
644 lines
17 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{}
|
|
|
|
var publicRouteAllowlist = []string{
|
|
"/api/health",
|
|
"/api/version",
|
|
"/api/agent/version",
|
|
"/api/agent/ws",
|
|
"/api/server/info",
|
|
"/api/security/status",
|
|
"/api/security/validate-bootstrap-token",
|
|
"/api/security/quick-setup",
|
|
"/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/system/verify-temperature-ssh",
|
|
"/api/system/ssh-config",
|
|
"/api/auto-register",
|
|
"/auth/cloud-handoff",
|
|
"/auth/trial-activate",
|
|
licensePurchaseActivationPath,
|
|
"/install.sh",
|
|
"/install.ps1",
|
|
"/download/pulse-agent",
|
|
}
|
|
|
|
var bareRouteAllowlist = []string{
|
|
"/api/agent-deploy/jobs/",
|
|
"/api/agent-deploy/preflights/",
|
|
"/api/agent/version",
|
|
"/api/agent/ws",
|
|
"/api/ai/oauth/callback",
|
|
"/api/auto-register",
|
|
"/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/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/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",
|
|
"/auth/trial-activate",
|
|
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/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/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/dashboard-summary",
|
|
"/api/resources/k8s/namespaces",
|
|
"/api/resources/stats",
|
|
"/api/resources/",
|
|
"/api/resources/{id}/facets",
|
|
"/api/resources/{id}/timeline",
|
|
"/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/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/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",
|
|
"POST /api/license/trial/start",
|
|
"GET /api/license/monitored-system-ledger",
|
|
"POST /api/license/monitored-system-ledger/explain",
|
|
"POST /api/license/monitored-system-ledger/preview",
|
|
"POST /api/upgrade-metrics/events",
|
|
"GET /api/upgrade-metrics/stats",
|
|
"GET /api/upgrade-metrics/health",
|
|
"GET /api/upgrade-metrics/config",
|
|
"PUT /api/upgrade-metrics/config",
|
|
"GET /api/admin/upgrade-metrics-funnel",
|
|
"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}/shares",
|
|
"GET /api/orgs/{id}/shares/incoming",
|
|
"POST /api/orgs/{id}/shares",
|
|
"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",
|
|
"/auth/trial-activate",
|
|
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",
|
|
"/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/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/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/chat",
|
|
"/api/ai/sessions",
|
|
"/api/ai/sessions/",
|
|
"/api/ai/approvals",
|
|
"/api/ai/approvals/",
|
|
"/api/ai/question/",
|
|
"/api/discovery",
|
|
"/api/discovery/status",
|
|
"/api/discovery/settings",
|
|
"/api/discovery/info/",
|
|
"/api/discovery/type/",
|
|
"/api/discovery/agent/",
|
|
"/api/discovery/",
|
|
"/api/agent/ws",
|
|
"/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",
|
|
}
|