package main import ( "fmt" "os" "path/filepath" "sort" ) func init() { register(Check{ Seq: 20, ID: "2", Title: "RPC methods registered in RBAC interceptor", Run: func(ctx *ScanContext, rep *Reporter) { // Check 2: RPC methods in RBAC interceptor fmt.Println("=== Check 2: RPC methods registered in RBAC interceptor ===") protoMethods := make([]string, 0) rbacMethods := make(map[string]string) protoMethodSet := make(map[string]bool) requiresRPCDiscovery := false var rbacSummaries []rbacTargetSummary for _, target := range ctx.backendTargets { rbacFile := filepath.Join(target.root, "internal/rbac/interceptor.go") hasCoreRBAC := fileExists(rbacFile) summary := rbacTargetSummary{ label: target.displayOrRoot(), roleByMethod: make(map[string]string), } if hasCoreRBAC { requiresRPCDiscovery = true summary.requiresRPCs = true targetProtoMethods, err := extractBufProcedures(target.protoRoot, target.allowedPackagePrefixes...) if err != nil { fmt.Printf(" ERROR: failed to parse proto files for %s: %v\n", target.displayOrRoot(), err) os.Exit(2) } summary.protoMethods = append(summary.protoMethods, targetProtoMethods...) for _, pm := range targetProtoMethods { if protoMethodSet[pm] { continue } protoMethodSet[pm] = true protoMethods = append(protoMethods, pm) } targetRBACMethods, err := extractRBACMethodRoles(rbacFile) if err != nil { fmt.Printf(" ERROR: failed to parse RBAC interceptor for %s: %v\n", target.displayOrRoot(), err) os.Exit(2) } for method, role := range targetRBACMethods { rbacMethods[method] = role summary.roleByMethod[method] = role } rbacSummaries = append(rbacSummaries, summary) continue } if len(target.allowedPackagePrefixes) > 0 { requiresRPCDiscovery = true summary.requiresRPCs = true } targetProtoMethods, err := extractBufProcedures(target.root) if err != nil { fmt.Printf(" ERROR: failed to parse proto files for %s: %v\n", target.displayOrRoot(), err) os.Exit(2) } summary.protoMethods = append(summary.protoMethods, targetProtoMethods...) for _, pm := range targetProtoMethods { if protoMethodSet[pm] { continue } protoMethodSet[pm] = true protoMethods = append(protoMethods, pm) } targetRoleMethods, err := extractPluginRoleMethods(target.root) if err != nil { fmt.Printf(" ERROR: failed to parse RBAC roles for %s: %v\n", target.displayOrRoot(), err) os.Exit(2) } for method, role := range targetRoleMethods { rbacMethods[method] = role summary.roleByMethod[method] = role } rbacSummaries = append(rbacSummaries, summary) } for _, target := range ctx.pluginTargets { summary := rbacTargetSummary{ label: target.display, roleByMethod: make(map[string]string), } pluginProtoMethods, err := extractBufProcedures(target.root) if err != nil { fmt.Printf(" ERROR: failed to parse plugin proto files for %s: %v\n", target.display, err) os.Exit(2) } summary.protoMethods = append(summary.protoMethods, pluginProtoMethods...) for _, pm := range pluginProtoMethods { if protoMethodSet[pm] { continue } protoMethodSet[pm] = true protoMethods = append(protoMethods, pm) } pluginRoleMethods, err := extractPluginRoleMethods(target.root) if err != nil { fmt.Printf(" ERROR: failed to parse plugin RBAC roles for %s: %v\n", target.display, err) os.Exit(2) } for method, role := range pluginRoleMethods { rbacMethods[method] = role summary.roleByMethod[method] = role } rbacSummaries = append(rbacSummaries, summary) } if len(protoMethods) == 0 { if requiresRPCDiscovery { fmt.Println(" ERROR: no RPC procedures found in proto sources or generated Connect files") os.Exit(2) } fmt.Println(" SKIP: no proto-backed RPC procedures found in scanned target(s)") } else { var missing []string for _, m := range protoMethods { if isExcludedServiceMethod(m) { continue } if _, ok := rbacMethods[m]; !ok { missing = append(missing, m) } } var stale []string for m := range rbacMethods { if !protoMethodSet[m] { if isExcludedServiceMethod(m) { continue } stale = append(stale, m) } } sort.Strings(stale) for i := range rbacSummaries { summary := &rbacSummaries[i] for _, method := range summary.protoMethods { if isExcludedServiceMethod(method) { continue } if _, ok := summary.roleByMethod[method]; !ok { summary.missingCount++ } } for method := range summary.roleByMethod { if isExcludedServiceMethod(method) { continue } if !containsString(summary.protoMethods, method) { summary.staleCount++ } } } for _, summary := range rbacSummaries { if len(summary.protoMethods) == 0 { if summary.requiresRPCs { fmt.Printf(" ERROR: %s — no RPC procedures found\n", summary.label) } else { fmt.Printf(" OK: %s — no proto-backed RPC procedures\n", summary.label) } continue } fmt.Printf(" OK: %s — %d RPC methods discovered\n", summary.label, len(summary.protoMethods)) printRoleBreakdown(countRBACRoles(summary.protoMethods, summary.roleByMethod)) if summary.missingCount > 0 { fmt.Printf(" missing RBAC entries: %d\n", summary.missingCount) } if summary.staleCount > 0 { fmt.Printf(" stale RBAC entries: %d\n", summary.staleCount) } } if len(missing) > 0 { fmt.Printf(" FAIL: %d RPC method(s) NOT in MethodRoles (will get permission_denied):\n", len(missing)) for _, m := range missing { fmt.Printf(" - %s\n", m) } rep.Fail() } if len(stale) > 0 { fmt.Printf(" WARN: %d RBAC entries have no matching proto method (stale?):\n", len(stale)) for _, m := range stale { fmt.Printf(" - %s\n", m) } } if len(missing) == 0 && len(stale) == 0 { fmt.Printf(" OK: All %d RPC methods are registered in MethodRoles\n", len(protoMethods)) } else if len(missing) == 0 { fmt.Printf(" OK: All %d RPC methods are registered (but %d stale entries exist)\n", len(protoMethods), len(stale)) } } fmt.Println() }, }) }