// check-safety verifies critical backend and frontend safety invariants. // // Run: go run ./cmd/check-safety . // // Checks: // 1. No secret env var reads outside config.Load() // 2. All RPC methods registered in RBAC interceptor // 3. Go code compiles and passes go fix, golangci-lint --fix, go vet, and strict lint checks // 4. Frontend code is auto-formatted, lint-clean, and typecheck-clean // 5. Frontend uses generated ConnectRPC hooks // 6. No hardcoded colors in frontend // 7. No useState for tab state in route files // 8. Import @block-ninja/api from subpaths only // 9. No npm/yarn lockfiles (pnpm only) // // 10. Button automation attributes // // 11. Plugin frontend discovery // 12. No placeholder code; only shipped features // 13. No reinvented utilities (use helpers) // 14. RPC query/mutation error handling // 15. No raw SQL outside sqlc/Bob // 16. No err.Error() leaked to HTTP clients // 17. Services/handlers in correct directories // 18. No TODO markers in production code // 19. Plugin segmentation (safety-rules.yml) // 19. Bearer token precedence over cookies in auth middleware // 20. Tailwind v4 configuration (PostCSS plugin, CSS directives, @config) // 21. Plugin presets.json validation (type-safe unmarshal against theme.Theme) // 22. Standalone plugins must stay on the published SDK boundary (imports, go.mod, version) // 23. sqlc uuid overrides in standalone plugins and cmd configs must use github.com/google/uuid // 24. Warn on any usage in Go and TypeScript // 25. sqlc compile and buf generate must succeed for scanned roots that define them // 26. Orchestrator backend tests must pass when checking the core BlockNinja repo // 27. No hand-rolled HTML sanitization — use bluemonday // 28. Public page handlers inject admin toolbar data // 29. No -buildmode=plugin targets in ported plugin repos (disabled until WO-WZ-017) package main import ( "fmt" "os" "path/filepath" "sort" "strings" ) var secretEnvVars = map[string]bool{ "JWT_SECRET": true, "PUBLIC_JWT_SECRET": true, "PLATFORM_SECRETS_KEY": true, "AI_KEY_ENCRYPTION_KEY": true, "STORAGE_ENCRYPTION_KEY": true, "COMM_KEY_ENCRYPTION_KEY": true, "ORCHESTRATOR_SECRET": true, "SMTP_PASSWORD": true, "CMS_BUILDER_SECRET": true, "VAULT_TOKEN": true, "VAULT_ROLE_ID": true, "VAULT_SECRET_ID": true, "CALCOM_API_KEY": true, } var exemptSecretEnvVars = map[string]bool{ "VAULT_TOKEN": true, // OpenBao/Vault clients may need ambient token access outside startup. } // Files allowed to read secret env vars func isAllowedEnvReader(path string) bool { return strings.HasSuffix(path, "config/config.go") || strings.HasSuffix(path, "_test.go") } // Services that are internal (not routed through the RBAC interceptor) // Services that use their own auth middleware instead of the RBAC interceptor var excludedServices = map[string]bool{ "ManagementService": true, // Orchestrator-to-CMS, uses X-Orchestrator-Secret // Plugin-managed services: RBAC entries provided via MergeMethodRoles at runtime "CommunityReviewsService": true, "CommunityModerationService": true, "UserEngagementService": true, "DataSuggestionsService": true, "WikiService": true, // Symposium plugin } const maxAnyWarningsPrinted = 120 func main() { ctx := buildScanContext() rep := &Reporter{verbose: ctx.verbose} // Checks self-register via init(); run them in declared Seq order. sort.Slice(registry, func(i, j int) bool { return registry[i].Seq < registry[j].Seq }) for i := range registry { rep.begin(registry[i].Seq, registry[i].ID, registry[i].Title) registry[i].Run(ctx, rep) rep.end() } rep.render() os.Exit(rep.exitCode) } func buildScanContext() *ScanContext { targetDir := "." var pluginPageDirs []string var pluginRoots []string orchestratorOnly := false allAny := false verbose := false // Parse args: first positional arg is backendDir. Plugin frontend paths may be passed // as direct source dirs via --plugin-pages or as plugin roots via --plugin-dir. args := os.Args[1:] for i := 0; i < len(args); i++ { if args[i] == "--orchestrator" { orchestratorOnly = true } else if args[i] == "--all-any" { allAny = true } else if args[i] == "--verbose" || args[i] == "-v" { verbose = true } else if args[i] == "--plugin-pages" { // Consume all following args until next flag or end for i++; i < len(args) && !strings.HasPrefix(args[i], "--"); i++ { pluginPageDirs = append(pluginPageDirs, args[i]) } i-- // back up so outer loop's i++ doesn't skip } else if args[i] == "--plugin-dir" || args[i] == "--plugin-dirs" { for i++; i < len(args) && !strings.HasPrefix(args[i], "--"); i++ { pluginRoots = append(pluginRoots, args[i]) } i-- } else if !strings.HasPrefix(args[i], "--") && targetDir == "." { targetDir = args[i] } } cwd, err := os.Getwd() if err != nil { fmt.Printf(" ERROR: failed to resolve working directory: %v\n", err) os.Exit(2) } targetDir = defaultScanTargetDir(targetDir, pluginRoots, cwd) // If the target is a plugin directory (has plugin.mod) and no explicit plugin roots were // passed, register it automatically so ninjatpl and other plugin-specific checks run. if len(pluginRoots) == 0 { if abs, absErr := filepath.Abs(targetDir); absErr == nil && fileExists(filepath.Join(abs, "plugin.mod")) { pluginRoots = append(pluginRoots, abs) } } backendDir, repoRoot, err := resolveScanRoots(targetDir) if err != nil { fmt.Printf(" ERROR: failed to resolve scan roots: %v\n", err) os.Exit(2) } if orchestratorOnly { orchRoot := orchestratorRepoRoot() orchBackend := filepath.Join(orchRoot, "backend") if orchRoot == "" || !dirExists(orchBackend) { fmt.Printf(" ERROR: orchestrator repo not found at %s\n", orchRoot) os.Exit(2) } backendDir = orchBackend repoRoot = orchRoot } scanPath, err := filepath.Abs(targetDir) if err != nil { scanPath = targetDir } if orchestratorOnly { fmt.Printf("check-safety %s [orchestrator only]\n", repoRoot) } else { fmt.Printf("check-safety %s\n", scanPath) } if !orchestratorOnly && len(pluginRoots) == 0 && len(pluginPageDirs) == 0 { blockNinjaRepo := blockNinjaRepoRoot() if samePath(scanPath, blockNinjaRepo) || samePath(scanPath, filepath.Join(blockNinjaRepo, "backend")) { fmt.Println("(for a plugin: use --plugin-dir or run in the plugin directory)") } } includeCoreTargets := shouldIncludeCoreTargets(targetDir, backendDir, pluginRoots, pluginPageDirs) backendTargets := collectBackendScanTargets(repoRoot, backendDir, includeCoreTargets) pluginTargets, unresolvedPluginRoots := resolvePluginTargets(repoRoot, pluginRoots) resolvedPluginTargets := pluginTargets pluginTargets = filterPluginTargets(pluginTargets, backendDir) frontendTargets := collectFrontendScanTargets(repoRoot, backendDir, pluginTargets, includeCoreTargets) frontendTargets = appendManualFrontendTargets(repoRoot, frontendTargets, pluginPageDirs) return &ScanContext{ repoRoot: repoRoot, backendDir: backendDir, orchestratorOnly: orchestratorOnly, includeCoreTargets: includeCoreTargets, allAny: allAny, verbose: verbose, backendTargets: backendTargets, pluginTargets: pluginTargets, resolvedPluginTargets: resolvedPluginTargets, frontendTargets: frontendTargets, unresolvedPluginRoots: unresolvedPluginRoots, pluginPageDirs: pluginPageDirs, } }