Check 2 (RPC methods registered in RBAC interceptor) fired FAIL on core, flagging all 926 shared procedures as "NOT in MethodRoles". Core is the shared Go SDK: it carries the canonical proto (blockninja.v1, orchestrator.v1, helpdesk.v1) but serves nothing and has no interceptor, so there is no MethodRoles map to match against and every procedure looked missing. RBAC is genuinely enforced where these procedures are actually served, and check 2 already validates it there: cms covers blockninja.v1 (green) and orchestrator covers orchestrator.v1 (0 missing). The abi.v1 protos declare no services, and helpdesk.v1 is unmounted definitions. So scanning the SDK in isolation is a not-applicable result, not a finding. Detect the core SDK target (no serving package prefix, no interceptor.go, module path == core) and SKIP check 2 for it instead of folding its shared procedures into the missing-set. Serving backends (cms/orchestrator) and plugins are untouched — the guard short-circuits the moment a target has a package prefix or an interceptor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
242 lines
7.8 KiB
Go
242 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// coreSDKModulePath is the shared Go SDK that carries the canonical proto
|
|
// definitions (blockninja.v1, orchestrator.v1, helpdesk.v1) consumed by the
|
|
// serving backends. It has no connect server and no RBAC interceptor of its
|
|
// own — RBAC is enforced in the repos that actually serve these procedures
|
|
// (cms for blockninja.v1, orchestrator for orchestrator.v1), where check 2
|
|
// runs against their interceptors. Scanning the SDK in isolation would flag
|
|
// every shared procedure as "missing"; that is a not-applicable result, not a
|
|
// finding.
|
|
const coreSDKModulePath = "git.dev.alexdunmow.com/block/core"
|
|
|
|
// isDefinitionsOnlySDKTarget reports whether the backend target is the shared
|
|
// proto SDK (core): it carries proto but serves nothing, so it has neither an
|
|
// RBAC interceptor nor a designated serving package prefix. Such a target is
|
|
// out of scope for check 2 — see coreSDKModulePath.
|
|
func isDefinitionsOnlySDKTarget(target backendScanTarget) bool {
|
|
if len(target.allowedPackagePrefixes) > 0 {
|
|
return false // a designated serving backend (cms/orchestrator)
|
|
}
|
|
if fileExists(filepath.Join(target.root, "internal/rbac/interceptor.go")) {
|
|
return false // has a serving RBAC interceptor
|
|
}
|
|
modPath, err := readModulePath(target.root)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return modPath == coreSDKModulePath
|
|
}
|
|
|
|
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
|
|
var definitionsOnlySDKLabels []string
|
|
|
|
for _, target := range ctx.backendTargets {
|
|
// The shared proto SDK (core) serves nothing — RBAC is enforced
|
|
// in the consuming repos, where this check runs. Don't fold its
|
|
// procedures into the missing-set; check 2 is not applicable.
|
|
if isDefinitionsOnlySDKTarget(target) {
|
|
definitionsOnlySDKLabels = append(definitionsOnlySDKLabels, target.displayOrRoot())
|
|
continue
|
|
}
|
|
|
|
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")
|
|
}
|
|
if len(definitionsOnlySDKLabels) > 0 {
|
|
rep.Skip("%s — definitions-only proto SDK; RBAC enforced in the serving repos (cms/orchestrator), not here", strings.Join(definitionsOnlySDKLabels, ", "))
|
|
} else {
|
|
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))
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}
|