Static safety/lint runner for the BlockNinja codebase. ~25 invariant
checks across Go and frontend sources. Was at git.dev.alexdunmow.com:block/ninja
in backend/cmd/check-safety/ until the 2026-06-06 consolidation moved
the BlockNinja repos under a shared ~/src/blockninja/ parent.
This repo is the standalone extraction:
- Own go.mod (git.dev.alexdunmow.com/block/check-safety, go 1.26.4)
- Vendored internal/{helpers,theme} from CMS (Go's internal/ rule
blocks cross-module imports; vendoring is the workaround)
- CLI contract unchanged: `check-safety <target-dir> [--flags]`
- CMS Makefile shells into ../check-safety for safety-check /
install-safety-checker targets
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
215 lines
6.4 KiB
Go
215 lines
6.4 KiB
Go
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()
|
|
},
|
|
})
|
|
}
|