From fc9c2b24779d670d617af1be5fbd3cdd744455f6 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 4 Feb 2026 13:05:12 +0000 Subject: [PATCH] Add public paths inventory test --- internal/api/route_inventory_test.go | 1 + .../api/router_public_paths_inventory_test.go | 129 ++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 internal/api/router_public_paths_inventory_test.go diff --git a/internal/api/route_inventory_test.go b/internal/api/route_inventory_test.go index f6ea3f226..b4bbde1f4 100644 --- a/internal/api/route_inventory_test.go +++ b/internal/api/route_inventory_test.go @@ -205,6 +205,7 @@ var publicRouteAllowlist = []string{ "/api/health", "/api/version", "/api/agent/version", + "/api/agent/ws", "/api/server/info", "/api/security/status", "/api/security/validate-bootstrap-token", diff --git a/internal/api/router_public_paths_inventory_test.go b/internal/api/router_public_paths_inventory_test.go new file mode 100644 index 000000000..58a5b1564 --- /dev/null +++ b/internal/api/router_public_paths_inventory_test.go @@ -0,0 +1,129 @@ +package api + +import ( + "go/ast" + "go/parser" + "go/token" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" +) + +func TestRouterPublicPathsInventory(t *testing.T) { + literalPaths, dynamicPaths := parsePublicPaths(t) + + expectedLiterals := sliceToSet(t, publicPathsAllowlist, "public paths allowlist") + expectedDynamics := sliceToSet(t, publicPathsDynamicAllowlist, "public dynamic allowlist") + expectedPublic := sliceToSet(t, publicRouteAllowlist, "public route allowlist") + + actualLiterals := sliceToSet(t, literalPaths, "router public paths") + actualDynamics := sliceToSet(t, dynamicPaths, "router public dynamic paths") + + if missing := setDifference(actualLiterals, expectedLiterals); len(missing) > 0 { + t.Fatalf("public paths missing from allowlist: %s", strings.Join(sortedKeys(missing), ", ")) + } + if stale := setDifference(expectedLiterals, actualLiterals); len(stale) > 0 { + t.Fatalf("public allowlist contains paths not in router.go: %s", strings.Join(sortedKeys(stale), ", ")) + } + if missing := setDifference(actualDynamics, expectedDynamics); len(missing) > 0 { + t.Fatalf("public dynamic paths missing from allowlist: %s", strings.Join(sortedKeys(missing), ", ")) + } + if stale := setDifference(expectedDynamics, actualDynamics); len(stale) > 0 { + t.Fatalf("public dynamic allowlist contains paths not in router.go: %s", strings.Join(sortedKeys(stale), ", ")) + } + if missing := setDifference(expectedLiterals, expectedPublic); len(missing) > 0 { + t.Fatalf("publicPaths entries missing from public route allowlist: %s", strings.Join(sortedKeys(missing), ", ")) + } +} + +func parsePublicPaths(t *testing.T) ([]string, []string) { + t.Helper() + + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatalf("failed to locate test file path") + } + routerPath := filepath.Join(filepath.Dir(file), "router.go") + + fset := token.NewFileSet() + fileAST, err := parser.ParseFile(fset, routerPath, nil, 0) + if err != nil { + t.Fatalf("parse router.go: %v", err) + } + + var literalPaths []string + var dynamicPaths []string + var found bool + + ast.Inspect(fileAST, func(node ast.Node) bool { + assign, ok := node.(*ast.AssignStmt) + if !ok || len(assign.Lhs) != 1 || len(assign.Rhs) != 1 { + return true + } + ident, ok := assign.Lhs[0].(*ast.Ident) + if !ok || ident.Name != "publicPaths" { + return true + } + comp, ok := assign.Rhs[0].(*ast.CompositeLit) + if !ok { + return true + } + found = true + for _, elt := range comp.Elts { + switch v := elt.(type) { + case *ast.BasicLit: + if v.Kind != token.STRING { + continue + } + unquoted, err := strconv.Unquote(v.Value) + if err != nil { + unquoted = strings.Trim(v.Value, "`\"'") + } + literalPaths = append(literalPaths, unquoted) + case *ast.Ident: + dynamicPaths = append(dynamicPaths, v.Name) + case *ast.SelectorExpr: + dynamicPaths = append(dynamicPaths, selectorName(v)) + } + } + return false + }) + + if !found { + t.Fatalf("publicPaths slice not found in router.go") + } + + return literalPaths, dynamicPaths +} + +var publicPathsDynamicAllowlist = []string{ + "config.DefaultOIDCCallbackPath", +} + +var publicPathsAllowlist = []string{ + "/api/health", + "/api/security/status", + "/api/security/validate-bootstrap-token", + "/api/security/quick-setup", + "/api/version", + "/api/login", + "/api/oidc/login", + "/install-docker-agent.sh", + "/install-container-agent.sh", + "/download/pulse-docker-agent", + "/install-host-agent.sh", + "/install-host-agent.ps1", + "/uninstall-host-agent.sh", + "/uninstall-host-agent.ps1", + "/download/pulse-host-agent", + "/install.sh", + "/install.ps1", + "/download/pulse-agent", + "/api/agent/version", + "/api/agent/ws", + "/api/server/info", + "/api/install/install-docker.sh", + "/api/ai/oauth/callback", +}