From 182f7fd299ace32134ae8ce62cdf6ee59c807b95 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 21 May 2024 22:29:33 -0230 Subject: [PATCH] Start --- .../interfaces/Linter/LinterDataGrid.tsx | 7 +- .../NavigationBar/NavigationBar.utils.tsx | 12 +- apps/studio/data/lint/lint-query.ts | 17 ++ .../[ref]/database/performance-advisor.tsx | 93 ---------- .../[ref]/database/security-advisor.tsx | 161 ------------------ 5 files changed, 33 insertions(+), 257 deletions(-) delete mode 100644 apps/studio/pages/project/[ref]/database/performance-advisor.tsx delete mode 100644 apps/studio/pages/project/[ref]/database/security-advisor.tsx diff --git a/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx b/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx index 0cb134f48a7..5ca745ae57d 100644 --- a/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx +++ b/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx @@ -28,6 +28,7 @@ interface LinterDataGridProps { selectedLint: Lint | null setSelectedLint: (value: Lint | null) => void currentTab: LINTER_LEVELS + handleLintClick: (lint: Lint | null) => void } const LinterDataGrid = ({ @@ -36,6 +37,7 @@ const LinterDataGrid = ({ selectedLint, setSelectedLint, currentTab, + handleLintClick, }: LinterDataGridProps) => { const gridRef = useRef(null) const { ref } = useParams() @@ -147,7 +149,8 @@ const LinterDataGrid = ({ {...props} onClick={() => { if (typeof idx === 'number' && idx >= 0) { - setSelectedLint(props.row) + // setSelectedLint(props.row) + handleLintClick(props.row) gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx }) } }} @@ -173,7 +176,7 @@ const LinterDataGrid = ({ className="absolute top-3 right-3 px-1" icon={} onClick={() => { - setSelectedLint(null) + handleLintClick(null) }} /> diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 30ecb4d6c66..35aeb9e068d 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -12,7 +12,7 @@ import { TableEditor, } from 'icons' import { ICON_SIZE, ICON_STROKE_WIDTH } from './NavigationBar' -import { Settings, FileText, List } from 'lucide-react' +import { Settings, FileText, List, Lightbulb } from 'lucide-react' export const generateToolRoutes = (ref?: string, project?: Project): Route[] => { const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP @@ -103,6 +103,16 @@ export const generateOtherRoutes = (ref?: string, project?: Project): Route[] => const buildingUrl = `/project/${ref}/building` return [ + ...(IS_PLATFORM + ? [ + { + key: 'advisors', + label: 'Advisors', + icon: , + link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/advisors/security`), + }, + ] + : []), ...(IS_PLATFORM ? [ { diff --git a/apps/studio/data/lint/lint-query.ts b/apps/studio/data/lint/lint-query.ts index 220df3a12c2..d55b586dd65 100644 --- a/apps/studio/data/lint/lint-query.ts +++ b/apps/studio/data/lint/lint-query.ts @@ -39,6 +39,7 @@ index_ as ( indisvalid ) select + md5(fk.schema_name || fk.table_name || fk.fkey_name) AS id, 'unindexed_foreign_keys' as name, 'Unindexed foreign keys' as title, 'INFO' as level, @@ -53,6 +54,7 @@ select ) as detail, 'https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys' as remediation, jsonb_build_object( + 'id', md5(fk.schema_name || fk.table_name || fk.fkey_name), 'schema', fk.schema_name, 'name', fk.table_name, 'type', 'table', @@ -81,6 +83,7 @@ order by union all ( select + md5(n.nspname || c.relname) AS id, 'auth_users_exposed' as name, 'Exposed Auth Users' as title, 'ERROR' as level, @@ -198,6 +201,7 @@ with policies as ( and pa.polname = pb.policyname ) select + md5(schema_name || table_name || policy_name) AS id, 'auth_rls_initplan' as name, 'Auth RLS Initialization Plan' as title, 'WARN' as level, @@ -262,6 +266,7 @@ where union all ( select + md5(pgns.nspname || pgc.relname) AS id, 'no_primary_key' as name, 'No Primary Key' as title, 'INFO' as level, @@ -308,6 +313,7 @@ having union all ( select + md5(psui.schemaname || psui.relname || psui.indexrelname) AS id, 'unused_index' as name, 'Unused Index' as title, 'INFO' as level, @@ -351,6 +357,7 @@ where union all ( select + md5(n.nspname || c.relname || r.rolname || act.cmd) AS id, 'multiple_permissive_policies' as name, 'Multiple Permissive Policies' as title, 'WARN' as level, @@ -424,6 +431,7 @@ having union all ( select + md5(n.nspname || c.relname) AS id, 'policy_exists_rls_disabled' as name, 'Policy Exists RLS Disabled' as title, 'ERROR' as level, @@ -470,6 +478,7 @@ group by union all ( select + md5(n.nspname || c.relname), 'rls_enabled_no_policy' as name, 'RLS Enabled No Policy' as title, 'INFO' as level, @@ -516,6 +525,7 @@ group by union all ( select + md5(n.nspname || c.relname || replace(pi.indexdef, pi.indexname, '')) AS id, 'duplicate_index' as name, 'Duplicate Index' as title, 'WARN' as level, @@ -571,6 +581,7 @@ having union all ( select + md5(n.nspname || c.relname) AS id, 'security_definer_view' as name, 'Security Definer View' as title, 'ERROR' as level, @@ -623,6 +634,7 @@ where union all ( select + md5(n.nspname || p.proname || md5(p.prosrc)) AS id, 'function_search_path_mutable' as name, 'Function Search Path Mutable' as title, 'WARN' as level, @@ -663,6 +675,7 @@ where union all ( select + md5(n.nspname || c.relname) AS id, 'rls_disabled_in_public' as name, 'RLS Disabled in Public' as title, 'ERROR' as level, @@ -704,6 +717,7 @@ where union all ( select + md5('public' || pe.extname) AS id, 'extension_in_public' as name, 'Extension in Public' as title, 'WARN' as level, @@ -756,6 +770,7 @@ with policies as ( and pa.polname = pb.policyname ) select + md5(schema_name || table_name || policy_name) AS id, 'rls_references_user_metadata' as name, 'RLS references user metadata' as title, 'ERROR' as level, @@ -793,6 +808,7 @@ where union all ( select + md5(n.nspname || c.relname) AS id, 'materialized_view_in_api' as name, 'Materialized View in API' as title, 'WARN' as level, @@ -857,6 +873,7 @@ export const LINT_TYPES = [ export type LINT_TYPES = (typeof LINT_TYPES)[number] export type Lint = { + id: string name: LINT_TYPES level: 'ERROR' | 'WARN' | 'INFO' facing: string diff --git a/apps/studio/pages/project/[ref]/database/performance-advisor.tsx b/apps/studio/pages/project/[ref]/database/performance-advisor.tsx deleted file mode 100644 index e8e1c8e5f85..00000000000 --- a/apps/studio/pages/project/[ref]/database/performance-advisor.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useParams } from 'common' -import { lintInfoMap } from 'components/interfaces/Linter/Linter.utils' -import { DatabaseLayout } from 'components/layouts' -import { FormHeader } from 'components/ui/Forms' -import { Lint, useProjectLintsQuery } from 'data/lint/lint-query' -import { useSelectedProject } from 'hooks' -import { useState } from 'react' -import type { NextPageWithLayout } from 'types' -import { LoadingLine } from 'ui' - -import LintPageTabs from 'components/interfaces/Linter/LintPageTabs' -import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants' -import LinterDataGrid from 'components/interfaces/Linter/LinterDataGrid' -import LinterFilters from 'components/interfaces/Linter/LinterFilters' -import LinterPageFooter from 'components/interfaces/Linter/LinterPageFooter' - -const ProjectLints: NextPageWithLayout = () => { - const project = useSelectedProject() - const { preset } = useParams() - - // need to maintain a list of filters for each tab - const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([ - { level: LINTER_LEVELS.ERROR, filters: [] }, - { level: LINTER_LEVELS.WARN, filters: [] }, - { level: LINTER_LEVELS.INFO, filters: [] }, - ]) - - const [currentTab, setCurrentTab] = useState( - (preset as LINTER_LEVELS) ?? LINTER_LEVELS.ERROR - ) - const [selectedLint, setSelectedLint] = useState(null) - - const { data, isLoading, isRefetching, refetch } = useProjectLintsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const activeLints = data?.filter((x) => x.categories.includes('PERFORMANCE')) || [] - - const currentTabFilters = (filters.find((filter) => filter.level === currentTab)?.filters || - []) as string[] - - const filteredLints = activeLints - .filter((x) => x.level === currentTab) - .filter((x) => (currentTabFilters.length > 0 ? currentTabFilters.includes(x.name) : x)) - - const filterOptions = lintInfoMap - // only show filters for lint types which are present in the results and not ignored - .filter((item) => - activeLints.some((lint) => lint.name === item.name && lint.level === currentTab) - ) - .map((type) => ({ - name: type.title, - value: type.name, - })) - - return ( -
- - - - - - -
- ) -} - -ProjectLints.getLayout = (page) => {page} - -export default ProjectLints diff --git a/apps/studio/pages/project/[ref]/database/security-advisor.tsx b/apps/studio/pages/project/[ref]/database/security-advisor.tsx deleted file mode 100644 index 1321cc34799..00000000000 --- a/apps/studio/pages/project/[ref]/database/security-advisor.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { useState } from 'react' - -import { useParams } from 'common' -import LintPageTabs from 'components/interfaces/Linter/LintPageTabs' -import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants' -import { lintInfoMap } from 'components/interfaces/Linter/Linter.utils' -import LinterDataGrid from 'components/interfaces/Linter/LinterDataGrid' -import LinterFilters from 'components/interfaces/Linter/LinterFilters' -import LinterPageFooter from 'components/interfaces/Linter/LinterPageFooter' -import { DatabaseLayout } from 'components/layouts' -import { FormHeader } from 'components/ui/Forms' -import { useAuthConfigQuery } from 'data/auth/auth-config-query' -import { Lint, useProjectLintsQuery } from 'data/lint/lint-query' -import { useSelectedProject } from 'hooks' -import type { NextPageWithLayout } from 'types' -import { LoadingLine } from 'ui' - -const ProjectLints: NextPageWithLayout = () => { - const project = useSelectedProject() - const { preset } = useParams() - - // need to maintain a list of filters for each tab - const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([ - { level: LINTER_LEVELS.ERROR, filters: [] }, - { level: LINTER_LEVELS.WARN, filters: [] }, - { level: LINTER_LEVELS.INFO, filters: [] }, - ]) - - const [currentTab, setCurrentTab] = useState( - (preset as LINTER_LEVELS) ?? LINTER_LEVELS.ERROR - ) - const [selectedLint, setSelectedLint] = useState(null) - - const { - data, - isLoading: areLintsLoading, - isRefetching, - refetch: refetchLintsQuery, - } = useProjectLintsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const { - data: authConfig, - isLoading: isAuthConfigLoading, - refetch: refetchAuthConfigQuery, - } = useAuthConfigQuery({ - projectRef: project?.ref, - }) - - const isLoading = areLintsLoading || isAuthConfigLoading - - const refetch = () => { - refetchLintsQuery() - refetchAuthConfigQuery() - } - - let clientLints: Lint[] = [] - - // [Alaister]: checking this client side for speed, but should be moved into the query if possible - if (authConfig?.EXTERNAL_EMAIL_ENABLED) { - if (authConfig.MAILER_OTP_EXP > 3600) { - clientLints.push({ - name: 'auth_otp_long_expiry', - level: 'WARN', - facing: 'EXTERNAL', - categories: ['SECURITY'], - description: 'OTP expiry exceeds recommended threshold', - detail: - 'We have detected that you have enabled the email provider with the OTP expiry set to more than an hour. It is recommended to set this value to less than an hour.', - cache_key: 'auth_otp_long_expiry', - remediation: 'https://supabase.com/docs/guides/platform/going-into-prod#security', - metadata: { - type: 'auth', - entity: 'Auth', - }, - }) - } - - if (authConfig.EXTERNAL_PHONE_ENABLED && authConfig.SMS_OTP_LENGTH < 6) { - clientLints.push({ - name: 'auth_otp_short_length', - level: 'WARN', - facing: 'EXTERNAL', - categories: ['SECURITY'], - description: 'OTP length is less than recommended threshold', - detail: 'We have detected that you have set the OTP length to less than 6 characters', - cache_key: 'auth_otp_short_length', - remediation: 'https://supabase.com/docs/guides/platform/going-into-prod#security', - metadata: { - type: 'auth', - entity: 'Auth', - }, - }) - } - } - - const activeLints = [...(data ?? []), ...clientLints]?.filter((x) => - x.categories.includes('SECURITY') - ) - - const currentTabFilters = (filters.find((filter) => filter.level === currentTab)?.filters || - []) as string[] - - const filteredLints = activeLints - .filter((x) => x.level === currentTab) - .filter((x) => (currentTabFilters.length > 0 ? currentTabFilters.includes(x.name) : x)) - - const filterOptions = lintInfoMap - // only show filters for lint types which are present in the results and not ignored - .filter((item) => - activeLints.some((lint) => lint.name === item.name && lint.level === currentTab) - ) - .map((type) => ({ - name: type.title, - value: type.name, - })) - - return ( -
- - - - - - -
- ) -} - -ProjectLints.getLayout = (page) => {page} - -export default ProjectLints