package main import ( "path/filepath" "sort" ) func init() { register(Check{ Seq: 20, ID: "2", Title: "RPC methods registered in RBAC interceptor", Run: func(ctx *ScanContext, rep *Reporter) { 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 { rep.Fatal("failed to parse proto files for %s: %v", target.displayOrRoot(), err) } 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 { rep.Fatal("failed to parse RBAC interceptor for %s: %v", target.displayOrRoot(), err) } 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 { rep.Fatal("failed to parse proto files for %s: %v", target.displayOrRoot(), err) } 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 { rep.Fatal("failed to parse RBAC roles for %s: %v", target.displayOrRoot(), err) } 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 { rep.Fatal("failed to parse plugin proto files for %s: %v", target.display, err) } 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 { rep.Fatal("failed to parse plugin RBAC roles for %s: %v", target.display, err) } for method, role := range pluginRoleMethods { rbacMethods[method] = role summary.roleByMethod[method] = role } rbacSummaries = append(rbacSummaries, summary) } if len(protoMethods) == 0 { if requiresRPCDiscovery { rep.Fatal("no RPC procedures found in proto sources or generated Connect files") } rep.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 { rep.Fail("%s — no RPC procedures found", summary.label) } else { rep.OK("%s — no proto-backed RPC procedures", summary.label) } continue } rep.OK("%s — %d RPC methods discovered", summary.label, len(summary.protoMethods)) if summary.missingCount > 0 { rep.Findingf("missing RBAC entries: %d", summary.missingCount) } if summary.staleCount > 0 { rep.Findingf("stale RBAC entries: %d", summary.staleCount) } } if len(missing) > 0 { rep.Fail("%d RPC method(s) NOT in MethodRoles (will get permission_denied)", len(missing)) for _, m := range missing { rep.Findingf("- %s", m) } } if len(stale) > 0 { rep.Warn("%d RBAC entries have no matching proto method (stale?)", len(stale)) for _, m := range stale { rep.Findingf("- %s", m) } } if len(missing) == 0 && len(stale) == 0 { rep.OK("All %d RPC methods are registered in MethodRoles", len(protoMethods)) } else if len(missing) == 0 { rep.OK("All %d RPC methods are registered (but %d stale entries exist)", len(protoMethods), len(stale)) } } }, }) }