Reporter is now a collector: checks declare a verdict (OK/Skip/Warn/Fail/ Fatal) plus findings, and a single central render() prints output. Default output is silent on pass/skip — only FAIL/WARN/ERR checks print, followed by one tally line, so a clean run is two lines. --verbose restores full per-check output. All ~30 checks were converted to this API; orphaned guidance/label helpers (printPerTargetOKLines, per-check *Help blocks, colors_format.go) were removed. The any-usage check (2e) now defaults to only the unstaged working-tree diff (changed lines), via a new per-repo git-diff index in changedlines.go; --all-any restores the full scan. Not-a-git-repo / no-diff warns on nothing. Golden fixtures regenerated; integration tests updated to the new format; added unit tests for the diff index. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
200 lines
5.9 KiB
Go
200 lines
5.9 KiB
Go
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))
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}
|