package main import ( "os" "path/filepath" "sort" ) type pluginScanTarget struct { root string display string frontendDir string frontendLabel string } type backendScanTarget struct { root string display string protoRoot string allowedPackagePrefixes []string } type frontendScanTarget struct { dir string label string pluginRules bool managedConfigs bool } type rbacTargetSummary struct { label string // protoMethods is the SERVED procedure surface (filtered to services the // target actually mounts) — it drives the missing-RBAC (permission_denied) // finding. protoMethods []string // protoMethodsAll is the FULL declared procedure surface (unfiltered) — it // drives stale-entry detection, so an RBAC role for a real-but-unmounted // proto method is not reported as stale. protoMethodsAll []string roleByMethod map[string]string missingCount int staleCount int requiresRPCs bool } func (t backendScanTarget) displayOrRoot() string { if t.display != "" { return t.display } return t.root } func resolveScanRoots(target string) (backendDir string, repoRoot string, err error) { absTarget, err := filepath.Abs(target) if err != nil { return "", "", err } if dirExists(filepath.Join(absTarget, "backend", "cmd", "check-safety")) { return filepath.Join(absTarget, "backend"), absTarget, nil } // Consolidated CMS layout — Go module at backend/, frontend at web/src. // The clause above keys on backend/cmd/check-safety, which left the CMS // tree when check-safety was hoisted to its own repo; without this one // the CMS resolves as its own backend, web/ is classified as a plugin // frontend, and the strict plugin rules bypass allowedFrontendFiles. if fileExists(filepath.Join(absTarget, "backend", "go.mod")) && dirExists(filepath.Join(absTarget, "web", "src")) { return filepath.Join(absTarget, "backend"), absTarget, nil } // Orchestrator layout — same standalone-app shape as the CMS above, but the // frontend lives at frontend/src (not web/src) and proto/buf sit at the repo // root. Without this clause the orchestrator root resolves as its own backend // (backendDir == repoRoot): backend/go.mod is reported missing, and // frontend/src is treated as a plugin frontend so the plugin-only rules fire // on a first-class app. if fileExists(filepath.Join(absTarget, "backend", "go.mod")) && dirExists(filepath.Join(absTarget, "frontend", "src")) && (fileExists(filepath.Join(absTarget, "buf.yaml")) || dirExists(filepath.Join(absTarget, "proto"))) { return filepath.Join(absTarget, "backend"), absTarget, nil } if filepath.Base(absTarget) == "backend" && fileExists(filepath.Join(absTarget, "go.mod")) { parent := filepath.Clean(filepath.Join(absTarget, "..")) if fileExists(filepath.Join(parent, "buf.yaml")) || dirExists(filepath.Join(parent, "proto")) { return absTarget, parent, nil } } if dirExists(filepath.Join(absTarget, "cmd", "check-safety")) { parent := filepath.Clean(filepath.Join(absTarget, "..")) if fileExists(filepath.Join(parent, "buf.yaml")) { return absTarget, parent, nil } return absTarget, absTarget, nil } return absTarget, absTarget, nil } func defaultScanTargetDir(targetDir string, pluginRoots []string, cwd string) string { if len(pluginRoots) > 0 || targetDir != "." { return targetDir } if fileExists(filepath.Join(cwd, "plugin.mod")) { return cwd } return filepath.Join(blockNinjaRepoRoot(), "backend") } func resolvePluginTargets(repoRoot string, roots []string) (targets []pluginScanTarget, unresolved []string) { seenRoots := make(map[string]bool) seenFrontends := make(map[string]bool) for _, root := range roots { resolvedRoot, err := expandPath(root) if err != nil { unresolved = append(unresolved, shortPluginLabel(root)) continue } absRoot, err := filepath.Abs(resolvedRoot) if err != nil { unresolved = append(unresolved, shortPluginLabel(root)) continue } info, err := os.Stat(absRoot) if err != nil || !info.IsDir() { unresolved = append(unresolved, shortPluginLabel(root)) continue } if seenRoots[absRoot] { continue } seenRoots[absRoot] = true target := pluginScanTarget{ root: absRoot, display: shortPluginLabel(absRoot), } if frontendDir, ok := findPluginFrontendDir(absRoot); ok { if !seenFrontends[frontendDir] { target.frontendDir = frontendDir target.frontendLabel = target.display seenFrontends[frontendDir] = true } } targets = append(targets, target) } sort.Slice(targets, func(i, j int) bool { return targets[i].display < targets[j].display }) return targets, unresolved } func collectBackendScanTargets(repoRoot, backendDir string, includeCoreTargets bool) []backendScanTarget { var targets []backendScanTarget seen := make(map[string]bool) if !includeCoreTargets { return nil } addTarget := func(root string, allowedPrefixes []string) { if root == "" || seen[root] || !dirExists(root) { return } seen[root] = true targets = append(targets, backendScanTarget{ root: root, display: normalizeDisplayLabel(displayLintPath(repoRoot, root)), protoRoot: resolveProtoScanRoot(root), allowedPackagePrefixes: allowedPrefixes, }) } addTarget(backendDir, inferAllowedPackagePrefixes(repoRoot, backendDir)) sort.Slice(targets, func(i, j int) bool { return targets[i].displayOrRoot() < targets[j].displayOrRoot() }) return targets } func filterPluginTargets(targets []pluginScanTarget, backendDir string) []pluginScanTarget { filtered := make([]pluginScanTarget, 0, len(targets)) for _, target := range targets { if samePath(target.root, backendDir) { continue } filtered = append(filtered, target) } return filtered } func collectFrontendScanTargets(repoRoot, backendDir string, pluginTargets []pluginScanTarget, includeCoreTargets bool) []frontendScanTarget { var targets []frontendScanTarget seen := make(map[string]bool) addTarget := func(dir, label string, pluginRules bool, managedConfigs bool) { if dir == "" || seen[dir] || !dirExists(dir) { return } seen[dir] = true targets = append(targets, frontendScanTarget{ dir: dir, label: normalizeDisplayLabel(label), pluginRules: pluginRules, managedConfigs: managedConfigs, }) } if includeCoreTargets && samePath(backendDir, filepath.Join(repoRoot, "backend")) { addTarget(filepath.Join(repoRoot, "web", "src"), "web/src", false, false) addTarget(filepath.Join(orchestratorRepoRoot(), "frontend", "src"), "orchestrator/frontend/src", false, true) for _, pluginWebDir := range discoverInternalPluginWebDirs(backendDir) { rel, _ := filepath.Rel(repoRoot, pluginWebDir) addTarget(pluginWebDir, rel, true, true) } } else if includeCoreTargets { if frontendDir, ok := findPluginFrontendDir(backendDir); ok { addTarget(frontendDir, displayLintPath(repoRoot, frontendDir), true, true) } } for _, target := range pluginTargets { if target.frontendDir == "" { continue } addTarget(target.frontendDir, target.frontendLabel, true, true) } sort.Slice(targets, func(i, j int) bool { return targets[i].label < targets[j].label }) return targets } func shouldIncludeCoreTargets(targetDir, backendDir string, pluginRoots []string, pluginPageDirs []string) bool { if len(pluginRoots) == 0 && len(pluginPageDirs) == 0 { return true } blockNinjaRepo := blockNinjaRepoRoot() if samePath(targetDir, blockNinjaRepo) || samePath(targetDir, filepath.Join(blockNinjaRepo, "backend")) { return false } if samePath(backendDir, filepath.Join(blockNinjaRepo, "backend")) && (samePath(targetDir, filepath.Join(blockNinjaRepo, "backend")) || samePath(targetDir, blockNinjaRepo)) { return false } return true } func appendManualFrontendTargets(repoRoot string, targets []frontendScanTarget, dirs []string) []frontendScanTarget { if len(dirs) == 0 { return targets } seen := make(map[string]bool, len(targets)) for _, target := range targets { if target.dir == "" { continue } absDir, err := filepath.Abs(target.dir) if err != nil { continue } seen[absDir] = true } for _, dir := range dirs { absDir, err := filepath.Abs(dir) if err != nil || seen[absDir] { continue } seen[absDir] = true targets = append(targets, frontendScanTarget{ dir: absDir, label: normalizeDisplayLabel(displayLintPath(repoRoot, absDir)), pluginRules: true, managedConfigs: true, }) } sort.Slice(targets, func(i, j int) bool { return targets[i].label < targets[j].label }) return targets } func collectESLintDirectiveTargets(repoRoot string, frontendTargets []frontendScanTarget) []frontendScanTarget { var targets []frontendScanTarget seen := make(map[string]bool) addTarget := func(dir, label string) { if dir == "" || !dirExists(dir) { return } absDir, err := filepath.Abs(dir) if err != nil || seen[absDir] { return } seen[absDir] = true targets = append(targets, frontendScanTarget{ dir: absDir, label: normalizeDisplayLabel(label), }) } for _, target := range frontendTargets { addTarget(target.dir, target.label) } if samePath(repoRoot, blockNinjaRepoRoot()) { addTarget(filepath.Join(repoRoot, "packages", "ui", "src"), "packages/ui/src") } sort.Slice(targets, func(i, j int) bool { return targets[i].label < targets[j].label }) return targets } func discoverInternalPluginWebDirs(backendDir string) []string { pluginsDir := filepath.Join(backendDir, "internal", "plugins") entries, err := os.ReadDir(pluginsDir) if err != nil { return nil } var dirs []string for _, entry := range entries { if !entry.IsDir() { continue } webDir := filepath.Join(pluginsDir, entry.Name(), "web") if dirLooksLikeFrontendSource(webDir) { abs, err := filepath.Abs(webDir) if err != nil { continue } dirs = append(dirs, abs) } } return dirs } func findPluginFrontendDir(root string) (string, bool) { for _, candidate := range []string{ root, filepath.Join(root, "src"), filepath.Join(root, "web"), filepath.Join(root, "web", "src"), filepath.Join(root, "frontend"), filepath.Join(root, "frontend", "src"), } { if !dirLooksLikeFrontendSource(candidate) { continue } absCandidate, err := filepath.Abs(candidate) if err != nil { continue } return absCandidate, true } return "", false } func inferAllowedPackagePrefixes(repoRoot, backendDir string) []string { switch { case samePath(repoRoot, orchestratorRepoRoot()): return []string{"orchestrator."} case samePath(backendDir, filepath.Join(repoRoot, "backend")): return []string{"blockninja."} default: return nil } } func resolveProtoScanRoot(backendDir string) string { parent := filepath.Clean(filepath.Join(backendDir, "..")) if fileExists(filepath.Join(parent, "buf.yaml")) || dirExists(filepath.Join(parent, "proto")) { return parent } return backendDir }