@jmruthers/pace-core 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/README.md +5 -403
- package/core-usage-manifest.json +93 -0
- package/cursor-rules/00-pace-core-compliance.mdc +128 -26
- package/cursor-rules/01-standards-compliance.mdc +49 -8
- package/cursor-rules/02-project-structure.mdc +6 -0
- package/cursor-rules/03-solid-principles.mdc +2 -0
- package/cursor-rules/04-testing-standards.mdc +2 -0
- package/cursor-rules/05-bug-reports-and-features.mdc +2 -0
- package/cursor-rules/06-code-quality.mdc +2 -0
- package/cursor-rules/07-tech-stack-compliance.mdc +2 -0
- package/cursor-rules/08-markup-quality.mdc +52 -27
- package/cursor-rules/09-rbac-compliance.mdc +462 -0
- package/cursor-rules/10-error-handling-patterns.mdc +179 -0
- package/cursor-rules/11-performance-optimization.mdc +169 -0
- package/cursor-rules/12-ci-cd-integration.mdc +150 -0
- package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
- package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-2N_tqbfq.d.ts} +1 -1
- package/dist/DataTable-LRJL4IRV.js +15 -0
- package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-BBH6Vqg7.d.ts} +72 -139
- package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
- package/dist/api-Y4MQWOFW.js +4 -0
- package/dist/audit-MYQXYZFU.js +3 -0
- package/dist/{chunk-J36DSWQK.js → chunk-2HGJFNAH.js} +8 -28
- package/dist/{chunk-OEWDTMG7.js → chunk-3O3WHILE.js} +38 -121
- package/dist/{chunk-M43Y4SSO.js → chunk-3QC3KRHK.js} +1 -14
- package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
- package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
- package/dist/chunk-4T7OBVTU.js +62 -0
- package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
- package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
- package/dist/{chunk-NN6WWZ5U.js → chunk-7TYHROIV.js} +579 -563
- package/dist/{chunk-M7MPQISP.js → chunk-A55DK444.js} +9 -16
- package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
- package/dist/{chunk-L4OXEN46.js → chunk-BVP2BCJF.js} +2 -16
- package/dist/chunk-C7NSAPTL.js +1 -0
- package/dist/{chunk-YKRAFF5K.js → chunk-FENMYN2U.js} +73 -149
- package/dist/{chunk-AVMLPIM7.js → chunk-FTCRZOG2.js} +284 -432
- package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
- package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
- package/dist/{chunk-I6DAQMWX.js → chunk-LAZMKTTF.js} +930 -891
- package/dist/{chunk-5EC5MEWX.js → chunk-MAGBIDNS.js} +77 -222
- package/dist/chunk-MBADTM7L.js +64 -0
- package/dist/chunk-OHIK3MIO.js +994 -0
- package/dist/{chunk-6SOIHG6Z.js → chunk-S7DKJPLT.js} +115 -44
- package/dist/{chunk-FMUCXFII.js → chunk-SD6WQY43.js} +1 -5
- package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
- package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
- package/dist/{chunk-FFQEQTNW.js → chunk-UIYSCEV7.js} +134 -45
- package/dist/{chunk-3LPHPB62.js → chunk-ZFYPMX46.js} +271 -87
- package/dist/{chunk-7JPAB3T5.js → chunk-ZS5VO5JB.js} +1989 -1283
- package/dist/components.d.ts +6 -6
- package/dist/components.js +57 -267
- package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
- package/dist/eslint-rules/index.cjs +22 -0
- package/dist/eslint-rules/rules/compliance.cjs +348 -0
- package/dist/eslint-rules/rules/components.cjs +113 -0
- package/dist/eslint-rules/rules/imports.cjs +102 -0
- package/dist/eslint-rules/rules/rbac.cjs +790 -0
- package/dist/eslint-rules/utils/helpers.cjs +42 -0
- package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
- package/dist/hooks.d.ts +5 -5
- package/dist/hooks.js +62 -270
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +36 -26
- package/dist/index.js +87 -690
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +8 -35
- package/dist/rbac/eslint-rules.d.ts +46 -44
- package/dist/rbac/eslint-rules.js +7 -4
- package/dist/rbac/index.d.ts +124 -594
- package/dist/rbac/index.js +14 -207
- package/dist/styles/index.js +2 -12
- package/dist/theming/runtime.js +3 -19
- package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
- package/dist/{types-CkbwOr4Y.d.ts → types-B-K_5VnO.d.ts} +4 -0
- package/dist/types-t9H8qKRw.d.ts +55 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +7 -94
- package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-COZ28Mvq.d.ts} +9 -9
- package/dist/utils.d.ts +24 -117
- package/dist/utils.js +54 -392
- package/docs/README.md +16 -6
- package/docs/api/README.md +4 -402
- package/docs/api/modules.md +454 -930
- package/docs/api-reference/components.md +3 -1
- package/docs/api-reference/deprecated.md +31 -6
- package/docs/api-reference/rpc-functions.md +78 -3
- package/docs/best-practices/accessibility.md +6 -3
- package/docs/getting-started/cursor-rules.md +3 -23
- package/docs/getting-started/dependencies.md +650 -0
- package/docs/getting-started/installation-guide.md +20 -7
- package/docs/getting-started/quick-start.md +23 -12
- package/docs/implementation-guides/permission-enforcement.md +4 -0
- package/docs/rbac/MIGRATION_GUIDE.md +819 -0
- package/docs/rbac/RBAC_CONTRACT.md +724 -0
- package/docs/rbac/README.md +12 -3
- package/docs/rbac/edge-functions-guide.md +376 -0
- package/docs/rbac/secure-client-protection.md +0 -34
- package/docs/standards/00-pace-core-compliance.md +967 -0
- package/docs/standards/01-standards-compliance.md +188 -0
- package/docs/standards/02-project-structure.md +985 -0
- package/docs/standards/03-solid-principles.md +39 -0
- package/docs/standards/04-testing-standards.md +36 -0
- package/docs/standards/05-bug-reports-and-features.md +27 -0
- package/docs/standards/{04-code-style-standard.md → 06-code-quality.md} +2 -0
- package/docs/standards/07-tech-stack-compliance.md +30 -0
- package/docs/standards/08-markup-quality.md +345 -0
- package/docs/standards/{07-rbac-and-rls-standard.md → 09-rbac-compliance.md} +149 -54
- package/docs/standards/10-error-handling-patterns.md +401 -0
- package/docs/standards/11-performance-optimization.md +348 -0
- package/docs/standards/12-ci-cd-integration.md +370 -0
- package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +192 -0
- package/docs/standards/README.md +62 -33
- package/docs/troubleshooting/organisation-context-setup.md +42 -19
- package/eslint-config-pace-core.cjs +20 -4
- package/package.json +31 -21
- package/scripts/audit/audit-compliance.cjs +1295 -0
- package/scripts/audit/audit-components.cjs +260 -0
- package/scripts/audit/audit-dependencies.cjs +395 -0
- package/scripts/audit/audit-rbac.cjs +954 -0
- package/scripts/audit/audit-standards.cjs +1268 -0
- package/scripts/audit/index.cjs +1898 -194
- package/scripts/install-cursor-rules.cjs +259 -8
- package/scripts/validate-master.js +1 -1
- package/src/__tests__/fixtures/supabase.ts +1 -1
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +1 -1
- package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +3 -3
- package/src/__tests__/helpers/component-test-utils.tsx +1 -1
- package/src/__tests__/helpers/supabaseMock.ts +2 -2
- package/src/__tests__/public-recipe-view.test.ts +38 -9
- package/src/components/Button/Button.tsx +5 -1
- package/src/components/ContextSelector/ContextSelector.tsx +42 -39
- package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
- package/src/components/DataTable/components/DataTableBody.tsx +55 -31
- package/src/components/DataTable/components/DataTableCore.tsx +186 -13
- package/src/components/DataTable/components/DataTableLayout.tsx +30 -5
- package/src/components/DataTable/components/EditFields.tsx +23 -3
- package/src/components/DataTable/components/EditableRow.tsx +7 -2
- package/src/components/DataTable/components/ImportModal.tsx +4 -6
- package/src/components/DataTable/components/RowComponent.tsx +12 -0
- package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
- package/src/components/DataTable/core/DataTableContext.tsx +1 -1
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
- package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
- package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
- package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
- package/src/components/DataTable/types.ts +5 -0
- package/src/components/DateTimeField/DateTimeField.tsx +20 -20
- package/src/components/DateTimeField/README.md +5 -2
- package/src/components/Dialog/Dialog.test.tsx +361 -318
- package/src/components/Dialog/Dialog.tsx +1154 -323
- package/src/components/Dialog/index.ts +3 -3
- package/src/components/FileDisplay/FileDisplay.test.tsx +45 -2
- package/src/components/FileDisplay/FileDisplay.tsx +28 -22
- package/src/components/Form/Form.test.tsx +9 -10
- package/src/components/Form/Form.tsx +369 -9
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
- package/src/components/LoginForm/LoginForm.tsx +2 -2
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
- package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +30 -41
- package/src/components/PaceAppLayout/README.md +10 -9
- package/src/components/PaceAppLayout/test-setup.tsx +40 -31
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
- package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
- package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
- package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
- package/src/components/Select/Select.tsx +23 -21
- package/src/components/Select/types.ts +1 -1
- package/src/components/UserMenu/UserMenu.test.tsx +38 -6
- package/src/components/UserMenu/UserMenu.tsx +39 -34
- package/src/components/index.ts +3 -4
- package/src/eslint-rules/index.cjs +22 -0
- package/src/eslint-rules/rules/compliance.cjs +348 -0
- package/src/eslint-rules/rules/components.cjs +113 -0
- package/src/eslint-rules/rules/imports.cjs +102 -0
- package/src/eslint-rules/rules/rbac.cjs +790 -0
- package/src/eslint-rules/utils/helpers.cjs +42 -0
- package/src/eslint-rules/utils/manifest-loader.cjs +75 -0
- package/src/hooks/__tests__/hooks.integration.test.tsx +6 -8
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
- package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
- package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
- package/src/hooks/public/usePublicEvent.ts +62 -190
- package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
- package/src/hooks/public/usePublicEventLogo.ts +19 -9
- package/src/hooks/useAppConfig.ts +26 -24
- package/src/hooks/useEventTheme.test.ts +211 -233
- package/src/hooks/useEventTheme.ts +19 -28
- package/src/hooks/useEvents.ts +11 -7
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useOrganisationPermissions.ts +9 -11
- package/src/hooks/useOrganisations.ts +13 -7
- package/src/hooks/useQueryCache.ts +0 -1
- package/src/hooks/useSessionDraft.ts +380 -0
- package/src/hooks/useSessionRestoration.ts +3 -1
- package/src/icons/index.ts +27 -0
- package/src/index.ts +16 -1
- package/src/providers/OrganisationProvider.tsx +23 -14
- package/src/providers/services/EventServiceProvider.tsx +1 -24
- package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +3 -0
- package/src/rbac/README.md +20 -20
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
- package/src/rbac/adapters.tsx +7 -295
- package/src/rbac/api.test.ts +44 -56
- package/src/rbac/api.ts +10 -17
- package/src/rbac/cache-invalidation.ts +0 -1
- package/src/rbac/compliance/index.ts +10 -0
- package/src/rbac/compliance/pattern-detector.ts +553 -0
- package/src/rbac/compliance/runtime-compliance.ts +22 -0
- package/src/rbac/components/AccessDenied.tsx +150 -0
- package/src/rbac/components/NavigationGuard.tsx +12 -20
- package/src/rbac/components/PagePermissionGuard.tsx +4 -24
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
- package/src/rbac/components/index.ts +3 -41
- package/src/rbac/eslint-rules.js +1 -1
- package/src/rbac/hooks/index.ts +0 -3
- package/src/rbac/hooks/permissions/index.ts +0 -3
- package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
- package/src/rbac/hooks/usePermissions.ts +0 -3
- package/src/rbac/hooks/useRBAC.test.ts +21 -3
- package/src/rbac/hooks/useRBAC.ts +4 -3
- package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
- package/src/rbac/hooks/useResolvedScope.ts +58 -140
- package/src/rbac/hooks/useResourcePermissions.test.ts +241 -60
- package/src/rbac/hooks/useResourcePermissions.ts +182 -63
- package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
- package/src/rbac/hooks/useRoleManagement.ts +147 -19
- package/src/rbac/hooks/useSecureSupabase.ts +4 -8
- package/src/rbac/index.ts +7 -9
- package/src/rbac/permissions.ts +17 -17
- package/src/rbac/utils/contextValidator.ts +45 -7
- package/src/services/AuthService.ts +132 -23
- package/src/services/EventService.ts +4 -97
- package/src/services/InactivityService.ts +155 -58
- package/src/services/OrganisationService.ts +7 -44
- package/src/services/__tests__/OrganisationService.test.ts +26 -8
- package/src/services/base/BaseService.ts +0 -3
- package/src/styles/core.css +4 -0
- package/src/types/database.generated.ts +4733 -3809
- package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
- package/src/utils/context/organisationContext.test.ts +13 -28
- package/src/utils/context/organisationContext.ts +21 -52
- package/src/utils/dynamic/dynamicUtils.ts +1 -1
- package/src/utils/file-reference/index.ts +39 -15
- package/src/utils/formatting/formatDateTime.test.ts +3 -2
- package/src/utils/formatting/formatTime.test.ts +3 -2
- package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
- package/src/utils/index.ts +4 -1
- package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
- package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
- package/src/utils/persistence/keyDerivation.ts +304 -0
- package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
- package/src/utils/security/secureStorage.ts +5 -5
- package/src/utils/storage/helpers.ts +3 -3
- package/src/utils/supabase/createBaseClient.ts +147 -0
- package/src/utils/timezone/timezone.test.ts +1 -2
- package/src/utils/timezone/timezone.ts +1 -1
- package/src/utils/validation/csrf.ts +4 -4
- package/cursor-rules/CHANGELOG.md +0 -119
- package/cursor-rules/README.md +0 -192
- package/dist/DataTable-E7YQZD7D.js +0 -175
- package/dist/DataTable-E7YQZD7D.js.map +0 -1
- package/dist/UnifiedAuthProvider-QPXO24B4.js +0 -18
- package/dist/UnifiedAuthProvider-QPXO24B4.js.map +0 -1
- package/dist/api-6LVZTHDS.js +0 -52
- package/dist/api-6LVZTHDS.js.map +0 -1
- package/dist/audit-V53FV5AG.js +0 -17
- package/dist/audit-V53FV5AG.js.map +0 -1
- package/dist/chunk-36LVWXB2.js +0 -227
- package/dist/chunk-36LVWXB2.js.map +0 -1
- package/dist/chunk-3LPHPB62.js.map +0 -1
- package/dist/chunk-5DRSZLL2.js.map +0 -1
- package/dist/chunk-5EC5MEWX.js.map +0 -1
- package/dist/chunk-63FOKYGO.js.map +0 -1
- package/dist/chunk-6SOIHG6Z.js.map +0 -1
- package/dist/chunk-7JPAB3T5.js.map +0 -1
- package/dist/chunk-ATKZM7RX.js +0 -2053
- package/dist/chunk-ATKZM7RX.js.map +0 -1
- package/dist/chunk-AVMLPIM7.js.map +0 -1
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/chunk-E66EQZE6.js.map +0 -1
- package/dist/chunk-FFQEQTNW.js.map +0 -1
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-G37KK66H.js.map +0 -1
- package/dist/chunk-I6DAQMWX.js.map +0 -1
- package/dist/chunk-J36DSWQK.js.map +0 -1
- package/dist/chunk-KQCRWDSA.js +0 -1
- package/dist/chunk-KQCRWDSA.js.map +0 -1
- package/dist/chunk-L4OXEN46.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-M43Y4SSO.js.map +0 -1
- package/dist/chunk-M7MPQISP.js.map +0 -1
- package/dist/chunk-NN6WWZ5U.js.map +0 -1
- package/dist/chunk-OEWDTMG7.js.map +0 -1
- package/dist/chunk-PWLANIRT.js.map +0 -1
- package/dist/chunk-QXHPKYJV.js.map +0 -1
- package/dist/chunk-VBXEHIUJ.js.map +0 -1
- package/dist/chunk-YKRAFF5K.js.map +0 -1
- package/dist/chunk-ZSAAAMVR.js.map +0 -1
- package/dist/components.js.map +0 -1
- package/dist/contextValidator-OOPCLPZW.js +0 -9
- package/dist/contextValidator-OOPCLPZW.js.map +0 -1
- package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
- package/dist/hooks.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/providers.js.map +0 -1
- package/dist/rbac/eslint-rules.js.map +0 -1
- package/dist/rbac/index.js.map +0 -1
- package/dist/styles/index.js.map +0 -1
- package/dist/theming/runtime.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/docs/standards/01-architecture-standard.md +0 -44
- package/docs/standards/02-api-and-rpc-standard.md +0 -39
- package/docs/standards/03-component-standard.md +0 -32
- package/docs/standards/05-security-standard.md +0 -44
- package/docs/standards/06-testing-and-docs-standard.md +0 -29
- package/docs/standards/pace-core-compliance.md +0 -432
- package/scripts/audit/core/checks/accessibility.cjs +0 -197
- package/scripts/audit/core/checks/api-usage.cjs +0 -191
- package/scripts/audit/core/checks/bundle.cjs +0 -142
- package/scripts/audit/core/checks/compliance.cjs +0 -2706
- package/scripts/audit/core/checks/config.cjs +0 -54
- package/scripts/audit/core/checks/coverage.cjs +0 -84
- package/scripts/audit/core/checks/dependencies.cjs +0 -994
- package/scripts/audit/core/checks/documentation.cjs +0 -268
- package/scripts/audit/core/checks/environment.cjs +0 -116
- package/scripts/audit/core/checks/error-handling.cjs +0 -340
- package/scripts/audit/core/checks/forms.cjs +0 -172
- package/scripts/audit/core/checks/heuristics.cjs +0 -68
- package/scripts/audit/core/checks/hooks.cjs +0 -334
- package/scripts/audit/core/checks/imports.cjs +0 -244
- package/scripts/audit/core/checks/performance.cjs +0 -325
- package/scripts/audit/core/checks/routes.cjs +0 -117
- package/scripts/audit/core/checks/state.cjs +0 -130
- package/scripts/audit/core/checks/structure.cjs +0 -65
- package/scripts/audit/core/checks/style.cjs +0 -584
- package/scripts/audit/core/checks/testing.cjs +0 -122
- package/scripts/audit/core/checks/typescript.cjs +0 -61
- package/scripts/audit/core/scanner.cjs +0 -199
- package/scripts/audit/core/utils.cjs +0 -137
- package/scripts/audit/reporters/console.cjs +0 -151
- package/scripts/audit/reporters/json.cjs +0 -54
- package/scripts/audit/reporters/markdown.cjs +0 -124
- package/scripts/audit-consuming-app.cjs +0 -86
- package/src/eslint-rules/pace-core-compliance.cjs +0 -510
- package/src/eslint-rules/pace-core-compliance.js +0 -638
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
- package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
- package/src/rbac/components/NavigationProvider.test.tsx +0 -481
- package/src/rbac/components/NavigationProvider.tsx +0 -345
- package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
- package/src/rbac/components/PagePermissionProvider.tsx +0 -279
- package/src/rbac/components/PermissionEnforcer.tsx +0 -312
- package/src/rbac/components/RoleBasedRouter.tsx +0 -440
- package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
- package/src/rbac/components/SecureDataProvider.tsx +0 -339
- package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
- package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
- package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +0 -90
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RBAC Compliance Audit Script
|
|
5
|
+
*
|
|
6
|
+
* Audits consuming apps for RBAC compliance according to 09-rbac-compliance.mdc.
|
|
7
|
+
* Checks for:
|
|
8
|
+
* - PagePermissionGuard usage on all protected pages
|
|
9
|
+
* - Wrapper components/functions around RBAC components
|
|
10
|
+
* - Direct RBAC RPC calls and table queries
|
|
11
|
+
* - Hardcoded role checks
|
|
12
|
+
* - RESOURCE_NAMES constants usage
|
|
13
|
+
* - AccessDenied component usage
|
|
14
|
+
* - enforcePermissions configuration
|
|
15
|
+
* - Edge Functions RBAC usage
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* const { runRBACAudit } = require('./audit-rbac.cjs');
|
|
19
|
+
* const issues = runRBACAudit(consumingAppPath);
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Recursively find all TypeScript/JavaScript files in a directory
|
|
27
|
+
*/
|
|
28
|
+
function findSourceFiles(dir, fileList = []) {
|
|
29
|
+
if (!fs.existsSync(dir)) {
|
|
30
|
+
return fileList;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const files = fs.readdirSync(dir);
|
|
34
|
+
|
|
35
|
+
files.forEach(file => {
|
|
36
|
+
const filePath = path.join(dir, file);
|
|
37
|
+
const stat = fs.statSync(filePath);
|
|
38
|
+
|
|
39
|
+
if (stat.isDirectory()) {
|
|
40
|
+
// Skip node_modules, dist, build, .git, etc.
|
|
41
|
+
if (!['node_modules', 'dist', 'build', '.git', '.next', '.vite', 'coverage', '.turbo'].includes(file)) {
|
|
42
|
+
findSourceFiles(filePath, fileList);
|
|
43
|
+
}
|
|
44
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
|
|
45
|
+
fileList.push(filePath);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return fileList;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get line number from index in content
|
|
54
|
+
*/
|
|
55
|
+
function getLineNumber(content, index) {
|
|
56
|
+
return content.substring(0, index).split('\n').length;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get code snippet around a match for context
|
|
61
|
+
*/
|
|
62
|
+
function getCodeSnippet(content, index, before = 30, after = 50) {
|
|
63
|
+
const start = Math.max(0, index - before);
|
|
64
|
+
const end = Math.min(content.length, index + after);
|
|
65
|
+
return content.substring(start, end).trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if content is in a comment or string
|
|
70
|
+
*/
|
|
71
|
+
function isInCommentOrString(content, index) {
|
|
72
|
+
const before = content.substring(0, index);
|
|
73
|
+
|
|
74
|
+
// Check for line comments
|
|
75
|
+
const lastLineComment = before.lastIndexOf('//');
|
|
76
|
+
const lastNewline = before.lastIndexOf('\n');
|
|
77
|
+
if (lastLineComment > lastNewline) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check for block comments
|
|
82
|
+
const lastBlockCommentStart = before.lastIndexOf('/*');
|
|
83
|
+
const lastBlockCommentEnd = before.lastIndexOf('*/');
|
|
84
|
+
if (lastBlockCommentStart > lastBlockCommentEnd) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for string literals (simple check)
|
|
89
|
+
const singleQuoteMatches = [...before.matchAll(/'/g)];
|
|
90
|
+
const doubleQuoteMatches = [...before.matchAll(/"/g)];
|
|
91
|
+
const backtickMatches = [...before.matchAll(/`/g)];
|
|
92
|
+
|
|
93
|
+
// Simple heuristic: if odd number of quotes before, might be in string
|
|
94
|
+
const inSingleQuote = singleQuoteMatches.length % 2 === 1;
|
|
95
|
+
const inDoubleQuote = doubleQuoteMatches.length % 2 === 1;
|
|
96
|
+
const inBacktick = backtickMatches.length % 2 === 1;
|
|
97
|
+
|
|
98
|
+
return inSingleQuote || inDoubleQuote || inBacktick;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if file imports a specific component/hook/util from pace-core
|
|
103
|
+
*/
|
|
104
|
+
function importsFromPaceCore(content, name) {
|
|
105
|
+
const patterns = [
|
|
106
|
+
new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core`),
|
|
107
|
+
new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/rbac`),
|
|
108
|
+
new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/components`),
|
|
109
|
+
new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/hooks`),
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
return patterns.some(pattern => pattern.test(content));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if a file is likely a page/route component
|
|
117
|
+
* Excludes: providers, routing components, shared/reusable components
|
|
118
|
+
*/
|
|
119
|
+
function isPageComponent(filePath, content) {
|
|
120
|
+
const fileName = path.basename(filePath);
|
|
121
|
+
const dirName = path.dirname(filePath);
|
|
122
|
+
const dirParts = dirName.split(path.sep);
|
|
123
|
+
|
|
124
|
+
// EXCLUDE: Provider components
|
|
125
|
+
if (dirParts.some(part => part.toLowerCase() === 'providers' || part.toLowerCase() === 'provider')) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// EXCLUDE: Routing components (files matching *Route.tsx or *Routes.tsx)
|
|
130
|
+
if (/Route(s)?\.(tsx?|jsx?)$/i.test(fileName)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// EXCLUDE: Shared/reusable components
|
|
135
|
+
if (dirParts.some(part => part.toLowerCase() === 'shared' || part.toLowerCase() === 'components')) {
|
|
136
|
+
// Only exclude if it's in a components directory (not pages/components)
|
|
137
|
+
const componentsIndex = dirParts.findIndex(part => part.toLowerCase() === 'components');
|
|
138
|
+
const pagesIndex = dirParts.findIndex(part => part.toLowerCase() === 'pages');
|
|
139
|
+
if (componentsIndex !== -1 && (pagesIndex === -1 || componentsIndex < pagesIndex)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// EXCLUDE: Files that are clearly providers (contain Provider in name and setup context)
|
|
145
|
+
if (/Provider\.(tsx?|jsx?)$/i.test(fileName) ||
|
|
146
|
+
/.*Provider.*\.(tsx?|jsx?)$/i.test(fileName)) {
|
|
147
|
+
// Check if it actually sets up providers (contains Provider components)
|
|
148
|
+
if (/<.*Provider|Provider\s*</.test(content) || /createContext|Context\.Provider/.test(content)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// INCLUDE: Files in pages/ directory (definitely pages)
|
|
154
|
+
if (dirParts.some(part => part.toLowerCase() === 'pages')) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// INCLUDE: Files matching *Page.tsx pattern (but not *Route.tsx which we already excluded)
|
|
159
|
+
if (/Page\.(tsx?|jsx?)$/i.test(fileName)) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// INCLUDE: Files in features/ directory that export page components
|
|
164
|
+
// (features often contain page components)
|
|
165
|
+
if (dirParts.some(part => part.toLowerCase() === 'features')) {
|
|
166
|
+
// Check if it exports a page-like component (has PagePermissionGuard or is a page)
|
|
167
|
+
if (/export\s+(function|const)\s+\w+Page/.test(content) ||
|
|
168
|
+
/PagePermissionGuard/.test(content)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// EXCLUDE: Files in app/ directory that are NOT pages
|
|
174
|
+
// (app/ can contain routes, providers, etc.)
|
|
175
|
+
if (dirParts.some(part => part.toLowerCase() === 'app')) {
|
|
176
|
+
// Only include if it's clearly a page (has PagePermissionGuard or matches page patterns)
|
|
177
|
+
if (/PagePermissionGuard/.test(content) ||
|
|
178
|
+
/export\s+(function|const)\s+\w+Page/.test(content)) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
// Otherwise exclude (likely a route, provider, or other non-page component)
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// EXCLUDE: Files that are clearly routing components (contain Routes, Route, Router)
|
|
186
|
+
if (/Routes|Router/.test(content) && !/PagePermissionGuard/.test(content)) {
|
|
187
|
+
// If it contains routing setup but no PagePermissionGuard, it's a routing component
|
|
188
|
+
if (/<Routes|<Route|<Router/.test(content)) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// EXCLUDE: Files that export routing components
|
|
194
|
+
if (/export\s+(function|const)\s+\w+Route(s)?/.test(content)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check 1: PagePermissionGuard Usage (Security Critical)
|
|
203
|
+
*/
|
|
204
|
+
function checkPagePermissionGuard(content, filePath, consumingAppPath) {
|
|
205
|
+
const issues = [];
|
|
206
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
207
|
+
|
|
208
|
+
// Only check page components
|
|
209
|
+
if (!isPageComponent(filePath, content)) {
|
|
210
|
+
return issues;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check if PagePermissionGuard is used
|
|
214
|
+
const hasPagePermissionGuard = /<PagePermissionGuard/.test(content) ||
|
|
215
|
+
/PagePermissionGuard\s*</.test(content);
|
|
216
|
+
|
|
217
|
+
// Check if RBAC hooks are imported (indicates this should be protected)
|
|
218
|
+
const hasRBACHooks = importsFromPaceCore(content, 'useCan') ||
|
|
219
|
+
importsFromPaceCore(content, 'useResourcePermissions') ||
|
|
220
|
+
importsFromPaceCore(content, 'usePermissions') ||
|
|
221
|
+
importsFromPaceCore(content, 'useRBAC');
|
|
222
|
+
|
|
223
|
+
// Check if component returns JSX (likely a page)
|
|
224
|
+
const returnsJSX = /return\s*\(/.test(content) || /return\s+</.test(content);
|
|
225
|
+
|
|
226
|
+
if (returnsJSX && !hasPagePermissionGuard) {
|
|
227
|
+
// Check if this is a public page (no RBAC hooks)
|
|
228
|
+
if (hasRBACHooks) {
|
|
229
|
+
// Definitely should be protected
|
|
230
|
+
issues.push({
|
|
231
|
+
type: 'rbacPageGuard',
|
|
232
|
+
file: relativePath,
|
|
233
|
+
line: 1,
|
|
234
|
+
message: 'Page component missing PagePermissionGuard wrapper. Pages using RBAC hooks must be protected.',
|
|
235
|
+
code: getCodeSnippet(content, content.indexOf('return')),
|
|
236
|
+
severity: 'error',
|
|
237
|
+
fix: 'Wrap page content with <PagePermissionGuard pageName="page-name" operation="read">',
|
|
238
|
+
});
|
|
239
|
+
} else {
|
|
240
|
+
// Might be a public page, but flag for review
|
|
241
|
+
issues.push({
|
|
242
|
+
type: 'rbacPageGuard',
|
|
243
|
+
file: relativePath,
|
|
244
|
+
line: 1,
|
|
245
|
+
message: 'Page component does not use PagePermissionGuard. Verify if this page should be protected.',
|
|
246
|
+
code: getCodeSnippet(content, content.indexOf('return') || 0),
|
|
247
|
+
severity: 'warning',
|
|
248
|
+
fix: 'If page should be protected, wrap with <PagePermissionGuard pageName="page-name" operation="read">',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check if PagePermissionGuard is used incorrectly (missing required props)
|
|
254
|
+
if (hasPagePermissionGuard) {
|
|
255
|
+
const pageGuardPattern = /<PagePermissionGuard[^>]*>/g;
|
|
256
|
+
let match;
|
|
257
|
+
while ((match = pageGuardPattern.exec(content)) !== null) {
|
|
258
|
+
if (isInCommentOrString(content, match.index)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const guardProps = match[0];
|
|
263
|
+
const hasPageName = /pageName\s*=/.test(guardProps);
|
|
264
|
+
const hasOperation = /operation\s*=/.test(guardProps);
|
|
265
|
+
|
|
266
|
+
if (!hasPageName || !hasOperation) {
|
|
267
|
+
issues.push({
|
|
268
|
+
type: 'rbacPageGuard',
|
|
269
|
+
file: relativePath,
|
|
270
|
+
line: getLineNumber(content, match.index),
|
|
271
|
+
message: 'PagePermissionGuard missing required props (pageName or operation)',
|
|
272
|
+
code: guardProps,
|
|
273
|
+
severity: 'error',
|
|
274
|
+
fix: 'Add required props: <PagePermissionGuard pageName="page-name" operation="read">',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return issues;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Check 2: Wrapper Components Around PagePermissionGuard (Security Risk)
|
|
285
|
+
*
|
|
286
|
+
* MIGRATED TO ESLINT: This check is now handled by 'no-rbac-wrapper-components' ESLint rule.
|
|
287
|
+
* Kept for reference only.
|
|
288
|
+
*/
|
|
289
|
+
function checkWrapperComponents_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
|
|
290
|
+
const issues = [];
|
|
291
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
292
|
+
|
|
293
|
+
// Pattern to find components that wrap PagePermissionGuard
|
|
294
|
+
// Look for function/const definitions that return PagePermissionGuard
|
|
295
|
+
const wrapperPattern = /(?:function|const|export\s+(?:function|const))\s+(\w+)\s*[=(][^)]*\)\s*\{[^}]*<PagePermissionGuard/gs;
|
|
296
|
+
let match;
|
|
297
|
+
|
|
298
|
+
while ((match = wrapperPattern.exec(content)) !== null) {
|
|
299
|
+
if (isInCommentOrString(content, match.index)) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const componentName = match[1];
|
|
304
|
+
const componentBody = match[0];
|
|
305
|
+
|
|
306
|
+
// Check if this component accepts pageName as prop (wrapper pattern)
|
|
307
|
+
if (/pageName\s*[:=]/.test(componentBody) || /\{[^}]*pageName/.test(componentBody)) {
|
|
308
|
+
issues.push({
|
|
309
|
+
type: 'rbacWrapperComponent',
|
|
310
|
+
file: relativePath,
|
|
311
|
+
line: getLineNumber(content, match.index),
|
|
312
|
+
message: `Wrapper component '${componentName}' detected around PagePermissionGuard. Must use PagePermissionGuard directly, not through wrappers.`,
|
|
313
|
+
code: getCodeSnippet(content, match.index, 0, 100),
|
|
314
|
+
severity: 'error',
|
|
315
|
+
fix: `Remove wrapper component and use PagePermissionGuard directly in pages.`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return issues;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Check 3: Wrapper Functions Around Permission Hooks (Security Risk)
|
|
325
|
+
*
|
|
326
|
+
* MIGRATED TO ESLINT: This check is now handled by 'no-rbac-wrapper-functions' ESLint rule.
|
|
327
|
+
* Kept for reference only.
|
|
328
|
+
*/
|
|
329
|
+
function checkWrapperFunctions_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
|
|
330
|
+
const issues = [];
|
|
331
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
332
|
+
|
|
333
|
+
// Pattern to find functions that use permission hooks
|
|
334
|
+
const permissionHooks = ['useCan', 'useResourcePermissions', 'usePermissions', 'useRBAC'];
|
|
335
|
+
|
|
336
|
+
permissionHooks.forEach(hookName => {
|
|
337
|
+
// Check if hook is imported
|
|
338
|
+
if (!importsFromPaceCore(content, hookName)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Find function definitions that might wrap the hook
|
|
343
|
+
// Pattern: function/const name = (...) => { ... useHook ... }
|
|
344
|
+
const functionPattern = new RegExp(`(?:function|const|export\\s+(?:function|const))\\s+(\\w+)\\s*[=(][^)]*\\)\\s*=>\\s*\\{[^}]*${hookName}`, 'gs');
|
|
345
|
+
let match;
|
|
346
|
+
|
|
347
|
+
while ((match = functionPattern.exec(content)) !== null) {
|
|
348
|
+
if (isInCommentOrString(content, match.index)) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const functionName = match[1];
|
|
353
|
+
const functionBody = match[0];
|
|
354
|
+
|
|
355
|
+
// Check if this function returns a permission check result (wrapper pattern)
|
|
356
|
+
// Look for patterns like: const canEdit = (...) => { const { can } = useCan(...); return can; }
|
|
357
|
+
if (/return\s+(can|hasPermission|isPermitted|canCreate|canUpdate|canDelete)/.test(functionBody) ||
|
|
358
|
+
/:\s*(can|hasPermission|isPermitted|boolean)/.test(functionBody)) {
|
|
359
|
+
issues.push({
|
|
360
|
+
type: 'rbacWrapperFunction',
|
|
361
|
+
file: relativePath,
|
|
362
|
+
line: getLineNumber(content, match.index),
|
|
363
|
+
message: `Wrapper function '${functionName}' detected around ${hookName} hook. Must use hooks directly, not through wrapper functions.`,
|
|
364
|
+
code: getCodeSnippet(content, match.index, 0, 150),
|
|
365
|
+
severity: 'error',
|
|
366
|
+
fix: `Remove wrapper function and use ${hookName} hook directly in components.`,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Also check for custom permission utility functions
|
|
372
|
+
const utilityPattern = /(?:function|const|export\s+(?:function|const))\s+(checkPermission|canEdit|canDelete|hasAccess|checkAccess)\s*[=(]/g;
|
|
373
|
+
let utilMatch;
|
|
374
|
+
while ((utilMatch = utilityPattern.exec(content)) !== null) {
|
|
375
|
+
if (isInCommentOrString(content, utilMatch.index)) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Check if this function uses permission hooks or RPC calls
|
|
380
|
+
const funcStart = utilMatch.index;
|
|
381
|
+
const funcEnd = content.indexOf('}', funcStart);
|
|
382
|
+
if (funcEnd === -1) continue;
|
|
383
|
+
|
|
384
|
+
const funcBody = content.substring(funcStart, funcEnd);
|
|
385
|
+
if (new RegExp(hookName).test(funcBody) || /rbac_check_permission/.test(funcBody)) {
|
|
386
|
+
issues.push({
|
|
387
|
+
type: 'rbacWrapperFunction',
|
|
388
|
+
file: relativePath,
|
|
389
|
+
line: getLineNumber(content, utilMatch.index),
|
|
390
|
+
message: `Custom permission utility function '${utilMatch[1]}' detected. Must use pace-core hooks directly, not custom utilities.`,
|
|
391
|
+
code: getCodeSnippet(content, utilMatch.index, 0, 150),
|
|
392
|
+
severity: 'error',
|
|
393
|
+
fix: `Remove custom utility and use ${hookName} hook directly.`,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
return issues;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Check 4: RESOURCE_NAMES Constants Usage (Type Safety)
|
|
404
|
+
*
|
|
405
|
+
* MIGRATED TO ESLINT: This check is now handled by 'rbac-use-resource-names-constants' ESLint rule.
|
|
406
|
+
* Kept for reference only.
|
|
407
|
+
*/
|
|
408
|
+
function checkResourceNamesConstants_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
|
|
409
|
+
const issues = [];
|
|
410
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
411
|
+
|
|
412
|
+
// Find all useResourcePermissions calls
|
|
413
|
+
const useResourcePermissionsPattern = /useResourcePermissions\s*\(/g;
|
|
414
|
+
let match;
|
|
415
|
+
|
|
416
|
+
while ((match = useResourcePermissionsPattern.exec(content)) !== null) {
|
|
417
|
+
if (isInCommentOrString(content, match.index)) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Get the argument to useResourcePermissions
|
|
422
|
+
const callStart = match.index;
|
|
423
|
+
const callEnd = content.indexOf(')', callStart);
|
|
424
|
+
if (callEnd === -1) continue;
|
|
425
|
+
|
|
426
|
+
const callContent = content.substring(callStart, callEnd + 1);
|
|
427
|
+
|
|
428
|
+
// Check if argument is a string literal (not a constant)
|
|
429
|
+
const stringLiteralPattern = /useResourcePermissions\s*\(\s*['"]([^'"]+)['"]/;
|
|
430
|
+
const stringMatch = callContent.match(stringLiteralPattern);
|
|
431
|
+
|
|
432
|
+
if (stringMatch) {
|
|
433
|
+
const resourceName = stringMatch[1];
|
|
434
|
+
|
|
435
|
+
// Check if RESOURCE_NAMES is imported
|
|
436
|
+
const hasResourceNames = importsFromPaceCore(content, 'RESOURCE_NAMES') ||
|
|
437
|
+
/RESOURCE_NAMES\s*\./.test(content);
|
|
438
|
+
|
|
439
|
+
issues.push({
|
|
440
|
+
type: 'rbacResourceNames',
|
|
441
|
+
file: relativePath,
|
|
442
|
+
line: getLineNumber(content, match.index),
|
|
443
|
+
message: `useResourcePermissions called with string literal '${resourceName}'. Must use RESOURCE_NAMES constant instead.`,
|
|
444
|
+
code: getCodeSnippet(content, match.index, 0, 80),
|
|
445
|
+
severity: 'error',
|
|
446
|
+
fix: `Import RESOURCE_NAMES from '@/config/resource-names' and use RESOURCE_NAMES.${resourceName.toUpperCase()} instead.`,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return issues;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Check 5: AccessDenied Component Usage (Consistency)
|
|
456
|
+
*/
|
|
457
|
+
function checkAccessDeniedComponent(content, filePath, consumingAppPath) {
|
|
458
|
+
const issues = [];
|
|
459
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
460
|
+
|
|
461
|
+
// Find custom access denied components
|
|
462
|
+
const customAccessDeniedPattern = /(?:function|const|export\s+(?:function|const))\s+(\w*(?:AccessDenied|PermissionDenied|Unauthorized|Forbidden)\w*)\s*[=(]/g;
|
|
463
|
+
let match;
|
|
464
|
+
|
|
465
|
+
while ((match = customAccessDeniedPattern.exec(content)) !== null) {
|
|
466
|
+
if (isInCommentOrString(content, match.index)) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const componentName = match[1];
|
|
471
|
+
|
|
472
|
+
// Skip if it's the pace-core AccessDenied import
|
|
473
|
+
if (importsFromPaceCore(content, 'AccessDenied') && componentName === 'AccessDenied') {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
issues.push({
|
|
478
|
+
type: 'rbacAccessDenied',
|
|
479
|
+
file: relativePath,
|
|
480
|
+
line: getLineNumber(content, match.index),
|
|
481
|
+
message: `Custom access denied component '${componentName}' detected. Must use AccessDenied from pace-core instead.`,
|
|
482
|
+
code: getCodeSnippet(content, match.index, 0, 100),
|
|
483
|
+
severity: 'warning',
|
|
484
|
+
fix: `Remove custom component and use AccessDenied from '@jmruthers/pace-core/rbac'`,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Check PagePermissionGuard fallback prop for custom components
|
|
489
|
+
const pageGuardPattern = /<PagePermissionGuard[^>]*fallback\s*=\s*\{?<(\w+)/g;
|
|
490
|
+
let fallbackMatch;
|
|
491
|
+
while ((fallbackMatch = pageGuardPattern.exec(content)) !== null) {
|
|
492
|
+
if (isInCommentOrString(content, fallbackMatch.index)) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const fallbackComponent = fallbackMatch[1];
|
|
497
|
+
if (fallbackComponent !== 'AccessDenied' && !importsFromPaceCore(content, fallbackComponent)) {
|
|
498
|
+
issues.push({
|
|
499
|
+
type: 'rbacAccessDenied',
|
|
500
|
+
file: relativePath,
|
|
501
|
+
line: getLineNumber(content, fallbackMatch.index),
|
|
502
|
+
message: `PagePermissionGuard uses custom fallback component '${fallbackComponent}'. Must use AccessDenied from pace-core.`,
|
|
503
|
+
code: getCodeSnippet(content, fallbackMatch.index, 0, 80),
|
|
504
|
+
severity: 'warning',
|
|
505
|
+
fix: `Use <AccessDenied /> from '@jmruthers/pace-core/rbac' as fallback prop.`,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return issues;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Check 6: Direct RBAC RPC Calls (Security Critical)
|
|
515
|
+
*
|
|
516
|
+
* MIGRATED TO ESLINT: This check is now handled by 'no-direct-rbac-rpc' ESLint rule.
|
|
517
|
+
* Kept for reference only.
|
|
518
|
+
*/
|
|
519
|
+
function checkDirectRBACRPC_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
|
|
520
|
+
const issues = [];
|
|
521
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
522
|
+
|
|
523
|
+
// Find direct RPC calls to RBAC functions
|
|
524
|
+
const rpcPattern = /\.rpc\s*\(\s*['"](rbac_[^'"]+)['"]/g;
|
|
525
|
+
let match;
|
|
526
|
+
|
|
527
|
+
while ((match = rpcPattern.exec(content)) !== null) {
|
|
528
|
+
if (isInCommentOrString(content, match.index)) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const rpcName = match[1];
|
|
533
|
+
|
|
534
|
+
// Check if this is the forbidden RPC
|
|
535
|
+
if (rpcName === 'rbac_check_permission_simplified' || rpcName.startsWith('rbac_')) {
|
|
536
|
+
// Check if it's in an Edge Function (where setupRBAC + isPermitted should be used)
|
|
537
|
+
const isEdgeFunction = /supabase\/functions/.test(filePath);
|
|
538
|
+
|
|
539
|
+
issues.push({
|
|
540
|
+
type: 'rbacDirectRPC',
|
|
541
|
+
file: relativePath,
|
|
542
|
+
line: getLineNumber(content, match.index),
|
|
543
|
+
message: `Direct RBAC RPC call '${rpcName}' detected. Must use pace-core API functions (isPermitted, isPermittedCached) instead.`,
|
|
544
|
+
code: getCodeSnippet(content, match.index, 0, 100),
|
|
545
|
+
severity: 'error',
|
|
546
|
+
fix: isEdgeFunction
|
|
547
|
+
? `Use isPermitted() API: import { setupRBAC, isPermitted } from 'npm:@jmruthers/pace-core@^0.6.0/rbac'; setupRBAC(supabase); const hasPermission = await isPermitted({...});`
|
|
548
|
+
: `Use isPermitted() or isPermittedCached() from '@jmruthers/pace-core/rbac' instead of direct RPC calls.`,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return issues;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Check 7: Direct RBAC Table Queries (Security Critical)
|
|
558
|
+
*
|
|
559
|
+
* MIGRATED TO ESLINT: This check is now handled by 'no-direct-rbac-table' ESLint rule.
|
|
560
|
+
* Kept for reference only.
|
|
561
|
+
*/
|
|
562
|
+
function checkDirectRBACTables_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
|
|
563
|
+
const issues = [];
|
|
564
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
565
|
+
|
|
566
|
+
// RBAC tables that must use useSecureSupabase
|
|
567
|
+
const rbacTables = [
|
|
568
|
+
'rbac_organisation_roles',
|
|
569
|
+
'rbac_event_app_roles',
|
|
570
|
+
'rbac_global_roles',
|
|
571
|
+
'rbac_apps',
|
|
572
|
+
'rbac_app_pages',
|
|
573
|
+
'rbac_page_permissions',
|
|
574
|
+
'rbac_user_profiles',
|
|
575
|
+
];
|
|
576
|
+
|
|
577
|
+
rbacTables.forEach(tableName => {
|
|
578
|
+
// Find .from('rbac_*') calls
|
|
579
|
+
const tablePattern = new RegExp(`\\.from\\s*\\(\\s*['"]${tableName}['"]`, 'g');
|
|
580
|
+
let match;
|
|
581
|
+
|
|
582
|
+
while ((match = tablePattern.exec(content)) !== null) {
|
|
583
|
+
if (isInCommentOrString(content, match.index)) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Check if this is using useSecureSupabase
|
|
588
|
+
const beforeMatch = content.substring(Math.max(0, match.index - 200), match.index);
|
|
589
|
+
const hasSecureSupabase = /useSecureSupabase\s*\(/.test(beforeMatch) ||
|
|
590
|
+
/secureSupabase/.test(beforeMatch) ||
|
|
591
|
+
/supabase\s*=\s*useSecureSupabase/.test(beforeMatch);
|
|
592
|
+
|
|
593
|
+
if (!hasSecureSupabase) {
|
|
594
|
+
issues.push({
|
|
595
|
+
type: 'rbacDirectTable',
|
|
596
|
+
file: relativePath,
|
|
597
|
+
line: getLineNumber(content, match.index),
|
|
598
|
+
message: `Direct query to RBAC table '${tableName}' detected. Must use useSecureSupabase() hook instead.`,
|
|
599
|
+
code: getCodeSnippet(content, match.index, 0, 100),
|
|
600
|
+
severity: 'error',
|
|
601
|
+
fix: `Use useSecureSupabase() from '@jmruthers/pace-core/rbac' for all RBAC table queries.`,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
return issues;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Check 8: Hardcoded Role Checks (Security Risk)
|
|
612
|
+
*
|
|
613
|
+
* MIGRATED TO ESLINT: This check is now handled by 'no-hardcoded-role-checks' ESLint rule.
|
|
614
|
+
* Kept for reference only.
|
|
615
|
+
*/
|
|
616
|
+
function checkHardcodedRoleChecks_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
|
|
617
|
+
const issues = [];
|
|
618
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
619
|
+
|
|
620
|
+
// Find hardcoded role comparisons
|
|
621
|
+
const rolePatterns = [
|
|
622
|
+
/(?:user|role|userRole)\.role\s*===\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
|
|
623
|
+
/role\s*===\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
|
|
624
|
+
/(?:user|role|userRole)\.role\s*!==\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
|
|
625
|
+
/role\s*!==\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
|
|
626
|
+
/(?:user|role|userRole)\.role\s*==\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
|
|
627
|
+
/role\s*==\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
|
|
628
|
+
];
|
|
629
|
+
|
|
630
|
+
rolePatterns.forEach(pattern => {
|
|
631
|
+
let match;
|
|
632
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
633
|
+
if (isInCommentOrString(content, match.index)) {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const roleValue = match[1];
|
|
638
|
+
|
|
639
|
+
issues.push({
|
|
640
|
+
type: 'rbacHardcodedRole',
|
|
641
|
+
file: relativePath,
|
|
642
|
+
line: getLineNumber(content, match.index),
|
|
643
|
+
message: `Hardcoded role check for '${roleValue}' detected. Must use pace-core APIs (useAccessLevel, getRoleContext) instead.`,
|
|
644
|
+
code: getCodeSnippet(content, match.index, 0, 80),
|
|
645
|
+
severity: 'error',
|
|
646
|
+
fix: `Use useAccessLevel(userId, scope) or getRoleContext({ userId, scope }) from '@jmruthers/pace-core/rbac' instead of hardcoded role checks.`,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
return issues;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Check 9: enforcePermissions Configuration (App Type)
|
|
656
|
+
*/
|
|
657
|
+
function checkEnforcePermissions(content, filePath, consumingAppPath) {
|
|
658
|
+
const issues = [];
|
|
659
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
660
|
+
|
|
661
|
+
// Find PaceAppLayout usage
|
|
662
|
+
const paceAppLayoutPattern = /<PaceAppLayout[^>]*>/g;
|
|
663
|
+
let match;
|
|
664
|
+
|
|
665
|
+
while ((match = paceAppLayoutPattern.exec(content)) !== null) {
|
|
666
|
+
if (isInCommentOrString(content, match.index)) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const layoutProps = match[0];
|
|
671
|
+
const hasEnforcePermissions = /enforcePermissions\s*=/.test(layoutProps);
|
|
672
|
+
|
|
673
|
+
if (!hasEnforcePermissions) {
|
|
674
|
+
issues.push({
|
|
675
|
+
type: 'rbacEnforcePermissions',
|
|
676
|
+
file: relativePath,
|
|
677
|
+
line: getLineNumber(content, match.index),
|
|
678
|
+
message: 'PaceAppLayout missing enforcePermissions prop. Must configure based on app type (event-based vs organisation-based).',
|
|
679
|
+
code: layoutProps,
|
|
680
|
+
severity: 'error',
|
|
681
|
+
fix: 'Add enforcePermissions prop: enforcePermissions={false} for event-based apps, enforcePermissions={true} for organisation-based apps.',
|
|
682
|
+
});
|
|
683
|
+
} else {
|
|
684
|
+
// Check if value is correct (heuristic: event-based apps typically have eventId in scope)
|
|
685
|
+
// This is a warning, not an error, as we can't definitively determine app type
|
|
686
|
+
const enforceTrue = /enforcePermissions\s*=\s*\{?\s*true\s*\}?/.test(layoutProps);
|
|
687
|
+
const enforceFalse = /enforcePermissions\s*=\s*\{?\s*false\s*\}?/.test(layoutProps);
|
|
688
|
+
|
|
689
|
+
// Check context for event-based indicators
|
|
690
|
+
const hasEventContext = /eventId|selectedEventId|useEvents/.test(content);
|
|
691
|
+
|
|
692
|
+
if (hasEventContext && enforceTrue) {
|
|
693
|
+
issues.push({
|
|
694
|
+
type: 'rbacEnforcePermissions',
|
|
695
|
+
file: relativePath,
|
|
696
|
+
line: getLineNumber(content, match.index),
|
|
697
|
+
message: 'PaceAppLayout has enforcePermissions={true} but app appears to be event-based. Event-based apps should use enforcePermissions={false} (pages handle checks).',
|
|
698
|
+
code: layoutProps,
|
|
699
|
+
severity: 'warning',
|
|
700
|
+
fix: 'Set enforcePermissions={false} for event-based apps. Pages should handle checks via PagePermissionGuard.',
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return issues;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Check 10: Edge Functions RBAC Usage (Security Critical)
|
|
711
|
+
*/
|
|
712
|
+
function checkEdgeFunctionsRBAC(consumingAppPath) {
|
|
713
|
+
const issues = [];
|
|
714
|
+
|
|
715
|
+
// Find Edge Function files
|
|
716
|
+
const functionsPath = path.join(consumingAppPath, 'supabase', 'functions');
|
|
717
|
+
if (!fs.existsSync(functionsPath)) {
|
|
718
|
+
return issues;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const edgeFunctionFiles = findSourceFiles(functionsPath);
|
|
722
|
+
|
|
723
|
+
edgeFunctionFiles.forEach(filePath => {
|
|
724
|
+
try {
|
|
725
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
726
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
727
|
+
|
|
728
|
+
// Check for setupRBAC call
|
|
729
|
+
const hasSetupRBAC = /setupRBAC\s*\(/.test(content);
|
|
730
|
+
|
|
731
|
+
// Check for isPermitted usage
|
|
732
|
+
const hasIsPermitted = /\bisPermitted\s*\(/.test(content);
|
|
733
|
+
|
|
734
|
+
// Check for custom RBAC helpers
|
|
735
|
+
const customHelperPattern = /(?:function|const|export\s+(?:function|const))\s+(checkPermission|canEdit|canDelete|hasAccess|checkAccess|rbacCheck)\s*[=(]/g;
|
|
736
|
+
const hasCustomHelper = customHelperPattern.test(content);
|
|
737
|
+
|
|
738
|
+
// Check for direct RPC calls
|
|
739
|
+
const hasDirectRPC = /\.rpc\s*\(\s*['"]rbac_/.test(content);
|
|
740
|
+
|
|
741
|
+
if (!hasSetupRBAC && (hasIsPermitted || hasCustomHelper || hasDirectRPC)) {
|
|
742
|
+
issues.push({
|
|
743
|
+
type: 'rbacEdgeFunction',
|
|
744
|
+
file: relativePath,
|
|
745
|
+
line: 1,
|
|
746
|
+
message: 'Edge Function uses RBAC but missing setupRBAC() call. Must call setupRBAC(supabase) before using isPermitted().',
|
|
747
|
+
code: '',
|
|
748
|
+
severity: 'error',
|
|
749
|
+
fix: 'Add: import { setupRBAC, isPermitted } from \'npm:@jmruthers/pace-core@^0.6.0/rbac\'; setupRBAC(supabase);',
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (hasCustomHelper) {
|
|
754
|
+
issues.push({
|
|
755
|
+
type: 'rbacEdgeFunction',
|
|
756
|
+
file: relativePath,
|
|
757
|
+
line: 1,
|
|
758
|
+
message: 'Edge Function contains custom RBAC helper functions. Must use isPermitted() API directly, not custom helpers.',
|
|
759
|
+
code: getCodeSnippet(content, content.indexOf('function') || 0, 0, 100),
|
|
760
|
+
severity: 'error',
|
|
761
|
+
fix: 'Remove custom RBAC helpers and use isPermitted() API from pace-core directly.',
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (hasDirectRPC) {
|
|
766
|
+
issues.push({
|
|
767
|
+
type: 'rbacEdgeFunction',
|
|
768
|
+
file: relativePath,
|
|
769
|
+
line: 1,
|
|
770
|
+
message: 'Edge Function uses direct RBAC RPC calls. Must use isPermitted() API instead.',
|
|
771
|
+
code: '',
|
|
772
|
+
severity: 'error',
|
|
773
|
+
fix: 'Replace direct RPC calls with isPermitted() API from pace-core.',
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (hasSetupRBAC && !hasIsPermitted && !hasDirectRPC && !hasCustomHelper) {
|
|
778
|
+
// setupRBAC called but no RBAC usage - might be incomplete
|
|
779
|
+
issues.push({
|
|
780
|
+
type: 'rbacEdgeFunction',
|
|
781
|
+
file: relativePath,
|
|
782
|
+
line: 1,
|
|
783
|
+
message: 'Edge Function calls setupRBAC() but does not use isPermitted(). Verify if RBAC checks are needed.',
|
|
784
|
+
code: '',
|
|
785
|
+
severity: 'warning',
|
|
786
|
+
fix: 'If RBAC checks are needed, use isPermitted() API. Otherwise, remove setupRBAC() call.',
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
} catch (error) {
|
|
791
|
+
// Skip files that can't be read
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
return issues;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Main audit function
|
|
800
|
+
*/
|
|
801
|
+
function runRBACAudit(consumingAppPath = process.cwd()) {
|
|
802
|
+
const srcPath = path.join(consumingAppPath, 'src');
|
|
803
|
+
const searchPath = fs.existsSync(srcPath) ? srcPath : consumingAppPath;
|
|
804
|
+
|
|
805
|
+
if (!fs.existsSync(searchPath)) {
|
|
806
|
+
return {
|
|
807
|
+
error: `Source directory not found at ${searchPath}`,
|
|
808
|
+
issues: {
|
|
809
|
+
rbacPageGuard: [],
|
|
810
|
+
rbacWrapperComponent: [],
|
|
811
|
+
rbacWrapperFunction: [],
|
|
812
|
+
rbacResourceNames: [],
|
|
813
|
+
rbacAccessDenied: [],
|
|
814
|
+
rbacDirectRPC: [],
|
|
815
|
+
rbacDirectTable: [],
|
|
816
|
+
rbacHardcodedRole: [],
|
|
817
|
+
rbacEnforcePermissions: [],
|
|
818
|
+
rbacEdgeFunction: [],
|
|
819
|
+
},
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Find all source files
|
|
824
|
+
const sourceFiles = findSourceFiles(searchPath);
|
|
825
|
+
|
|
826
|
+
if (sourceFiles.length === 0) {
|
|
827
|
+
return {
|
|
828
|
+
error: `No source files found in ${searchPath}`,
|
|
829
|
+
issues: {
|
|
830
|
+
rbacPageGuard: [],
|
|
831
|
+
rbacWrapperComponent: [],
|
|
832
|
+
rbacWrapperFunction: [],
|
|
833
|
+
rbacResourceNames: [],
|
|
834
|
+
rbacAccessDenied: [],
|
|
835
|
+
rbacDirectRPC: [],
|
|
836
|
+
rbacDirectTable: [],
|
|
837
|
+
rbacHardcodedRole: [],
|
|
838
|
+
rbacEnforcePermissions: [],
|
|
839
|
+
rbacEdgeFunction: [],
|
|
840
|
+
},
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const issues = {
|
|
845
|
+
rbacPageGuard: [],
|
|
846
|
+
rbacWrapperComponent: [],
|
|
847
|
+
rbacWrapperFunction: [],
|
|
848
|
+
rbacResourceNames: [],
|
|
849
|
+
rbacAccessDenied: [],
|
|
850
|
+
rbacDirectRPC: [],
|
|
851
|
+
rbacDirectTable: [],
|
|
852
|
+
rbacHardcodedRole: [],
|
|
853
|
+
rbacEnforcePermissions: [],
|
|
854
|
+
rbacEdgeFunction: [],
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
// Check each file
|
|
858
|
+
sourceFiles.forEach(filePath => {
|
|
859
|
+
try {
|
|
860
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
861
|
+
|
|
862
|
+
// Run all checks - wrap each in try-catch to prevent one failure from breaking others
|
|
863
|
+
try {
|
|
864
|
+
const pageGuardIssues = checkPagePermissionGuard(content, filePath, consumingAppPath);
|
|
865
|
+
if (Array.isArray(pageGuardIssues)) {
|
|
866
|
+
issues.rbacPageGuard.push(...pageGuardIssues);
|
|
867
|
+
}
|
|
868
|
+
} catch (e) {
|
|
869
|
+
// Skip this check for this file
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// NOTE: wrapperComponent, wrapperFunction, and resourceNames checks migrated to ESLint
|
|
873
|
+
// checkWrapperComponents → ESLint: no-rbac-wrapper-components
|
|
874
|
+
// checkWrapperFunctions → ESLint: no-rbac-wrapper-functions
|
|
875
|
+
// checkResourceNamesConstants → ESLint: rbac-use-resource-names-constants
|
|
876
|
+
|
|
877
|
+
try {
|
|
878
|
+
const accessDeniedIssues = checkAccessDeniedComponent(content, filePath, consumingAppPath);
|
|
879
|
+
if (Array.isArray(accessDeniedIssues)) {
|
|
880
|
+
issues.rbacAccessDenied.push(...accessDeniedIssues);
|
|
881
|
+
}
|
|
882
|
+
} catch (e) {
|
|
883
|
+
// Skip this check for this file
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// NOTE: directRPC, directTable, and hardcodedRole checks migrated to ESLint
|
|
887
|
+
// checkDirectRBACRPC → ESLint: no-direct-rbac-rpc
|
|
888
|
+
// checkDirectRBACTables → ESLint: no-direct-rbac-table
|
|
889
|
+
// checkHardcodedRoleChecks → ESLint: no-hardcoded-role-checks
|
|
890
|
+
|
|
891
|
+
try {
|
|
892
|
+
const enforcePermissionsIssues = checkEnforcePermissions(content, filePath, consumingAppPath);
|
|
893
|
+
if (Array.isArray(enforcePermissionsIssues)) {
|
|
894
|
+
issues.rbacEnforcePermissions.push(...enforcePermissionsIssues);
|
|
895
|
+
}
|
|
896
|
+
} catch (e) {
|
|
897
|
+
// Skip this check for this file
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
} catch (error) {
|
|
901
|
+
// Skip files that can't be read
|
|
902
|
+
console.warn(`Warning: Could not read ${filePath}: ${error.message}`);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
// Check Edge Functions (separate scan)
|
|
907
|
+
issues.rbacEdgeFunction.push(...checkEdgeFunctionsRBAC(consumingAppPath));
|
|
908
|
+
|
|
909
|
+
return {
|
|
910
|
+
issues,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Export for use by other scripts
|
|
915
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
916
|
+
module.exports = { runRBACAudit };
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// If run directly, output results
|
|
920
|
+
if (require.main === module) {
|
|
921
|
+
const consumingAppPath = process.argv[2] || process.cwd();
|
|
922
|
+
const result = runRBACAudit(consumingAppPath);
|
|
923
|
+
|
|
924
|
+
if (result.error) {
|
|
925
|
+
console.error(`Error: ${result.error}`);
|
|
926
|
+
process.exit(1);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const { issues } = result;
|
|
930
|
+
|
|
931
|
+
// Count total issues
|
|
932
|
+
const totalIssues = Object.values(issues).reduce((sum, arr) => sum + arr.length, 0);
|
|
933
|
+
|
|
934
|
+
if (totalIssues === 0) {
|
|
935
|
+
console.log('✅ No RBAC compliance issues found!');
|
|
936
|
+
process.exit(0);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
console.log(`\n❌ Found ${totalIssues} RBAC compliance issue(s):\n`);
|
|
940
|
+
|
|
941
|
+
// Group by type
|
|
942
|
+
Object.entries(issues).forEach(([type, typeIssues]) => {
|
|
943
|
+
if (typeIssues.length > 0) {
|
|
944
|
+
console.log(`\n${type}: ${typeIssues.length} issue(s)`);
|
|
945
|
+
typeIssues.forEach(issue => {
|
|
946
|
+
console.log(` ${issue.file}:${issue.line}`);
|
|
947
|
+
console.log(` ${issue.message}`);
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
|