Linter.utils.tsx499 lines · main
1import {
2 Box,
3 Clock,
4 Eye,
5 Lock,
6 LockIcon,
7 Ruler,
8 Scaling,
9 Table2,
10 TextSearch,
11 Unlock,
12 User,
13} from 'lucide-react'
14import Link from 'next/link'
15import { Badge, Button } from 'ui'
16
17import { asGraphqlExposureLint, GraphqlExposureLintCTA } from './GraphqlExposureLintCTA'
18import { LINTER_LEVELS, LintInfo } from '@/components/interfaces/Linter/Linter.constants'
19import { Lint, LINT_TYPES } from '@/data/lint/lint-query'
20import { DOCS_URL } from '@/lib/constants'
21
22export const lintInfoMap: LintInfo[] = [
23 {
24 name: 'unindexed_foreign_keys',
25 title: 'Unindexed foreign keys',
26 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
27 link: ({ projectRef, metadata }) =>
28 `/project/${projectRef}/database/indexes?schema=${encodeURIComponent(metadata?.schema ?? '')}`,
29 linkText: 'Create an index',
30 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0001_unindexed_foreign_keys`,
31 category: 'performance',
32 },
33 {
34 name: 'auth_users_exposed',
35 title: 'Exposed Auth Users',
36 icon: <Lock className="text-foreground-muted" size={15} strokeWidth={1.5} />,
37 link: ({ projectRef }) => `/project/${projectRef}/editor`,
38 linkText: 'View table',
39 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0002_auth_users_exposed`,
40 category: 'security',
41 },
42 {
43 name: 'auth_rls_initplan',
44 title: 'Auth RLS Initialization Plan',
45 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
46 link: ({ projectRef }) => `/project/${projectRef}/auth/policies`,
47 linkText: 'View policies',
48 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0003_auth_rls_initplan`,
49 category: 'performance',
50 },
51 {
52 name: 'no_primary_key',
53 title: 'No Primary Key',
54 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
55 link: ({ projectRef }) => `/project/${projectRef}/editor`,
56 linkText: 'View table',
57 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0004_no_primary_key`,
58 category: 'performance',
59 },
60 {
61 name: 'unused_index',
62 title: 'Unused Index',
63 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
64 link: ({ projectRef, metadata }) =>
65 `/project/${projectRef}/database/indexes?schema=${encodeURIComponent(metadata?.schema ?? '')}&table=${encodeURIComponent(metadata?.name ?? '')}`,
66 linkText: 'View index',
67 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0005_unused_index`,
68 category: 'performance',
69 },
70 {
71 name: 'multiple_permissive_policies',
72 title: 'Multiple Permissive Policies',
73 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
74 link: ({ projectRef, metadata }) =>
75 `/project/${projectRef}/auth/policies?schema=${encodeURIComponent(metadata?.schema ?? '')}&search=${encodeURIComponent(metadata?.name ?? '')}`,
76 linkText: 'View policies',
77 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0006_multiple_permissive_policies`,
78 category: 'performance',
79 },
80 {
81 name: 'policy_exists_rls_disabled',
82 title: 'Policy Exists RLS Disabled',
83 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
84 link: ({ projectRef, metadata }) =>
85 `/project/${projectRef}/auth/policies?schema=${encodeURIComponent(metadata?.schema ?? '')}&search=${encodeURIComponent(metadata?.name ?? '')}`,
86 linkText: 'View policies',
87 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0007_policy_exists_rls_disabled`,
88 category: 'security',
89 },
90 {
91 name: 'rls_enabled_no_policy',
92 title: 'RLS Enabled No Policy',
93 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
94 link: ({ projectRef, metadata }) =>
95 `/project/${projectRef}/auth/policies?schema=${encodeURIComponent(metadata?.schema ?? '')}&search=${encodeURIComponent(metadata?.name ?? '')}`,
96 linkText: 'View table',
97 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0008_rls_enabled_no_policy`,
98 category: 'security',
99 },
100 {
101 name: 'duplicate_index',
102 title: 'Duplicate Index',
103 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
104 link: ({ projectRef, metadata }) =>
105 `/project/${projectRef}/database/indexes?schema=${encodeURIComponent(metadata?.schema ?? '')}&table=${encodeURIComponent(metadata?.name ?? '')}`,
106 linkText: 'View index',
107 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0009_duplicate_index`,
108 category: 'performance',
109 },
110 {
111 name: 'security_definer_view',
112 title: 'Security Definer View',
113 icon: <Eye className="text-foreground-muted" size={15} strokeWidth={1.5} />,
114 link: () =>
115 `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0010_security_definer_view`,
116 linkText: 'View docs',
117 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0010_security_definer_view`,
118 category: 'security',
119 },
120 {
121 name: 'function_search_path_mutable',
122 title: 'Function Search Path Mutable',
123 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
124 link: ({ projectRef, metadata }) =>
125 `/project/${projectRef}/database/functions?schema=${encodeURIComponent(metadata?.schema ?? '')}&search=${encodeURIComponent(metadata?.name ?? '')}`,
126 linkText: 'View functions',
127 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0011_function_search_path_mutable`,
128 category: 'security',
129 },
130 {
131 name: 'auth_allow_anonymous_sign_ins',
132 title: 'Anonymous Sign-Ins Allowed',
133 icon: <User className="text-foreground-muted" size={15} strokeWidth={1} />,
134 link: ({ projectRef }) => `/project/${projectRef}/auth/providers`,
135 linkText: 'View settings',
136 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0012_auth_allow_anonymous_sign_ins`,
137 category: 'security',
138 },
139 {
140 name: 'rls_disabled_in_public',
141 title: 'RLS Disabled in Public',
142 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
143 link: ({ projectRef, metadata }) =>
144 `/project/${projectRef}/auth/policies?schema=${encodeURIComponent(metadata?.schema ?? '')}&search=${encodeURIComponent(metadata?.name ?? '')}`,
145 linkText: 'View policies',
146 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0013_rls_disabled_in_public`,
147 category: 'security',
148 },
149 {
150 name: 'extension_in_public',
151 title: 'Extension in Public',
152 icon: <Unlock className="text-foreground-muted" size={15} strokeWidth={1} />,
153 link: ({ projectRef, metadata }) =>
154 `/project/${projectRef}/database/extensions?filter=${encodeURIComponent(metadata?.name ?? '')}`,
155 linkText: 'View extension',
156 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0014_extension_in_public`,
157 category: 'security',
158 },
159 {
160 name: 'auth_otp_long_expiry',
161 title: 'Auth OTP Long Expiry',
162 icon: <Clock className="text-foreground-muted" size={15} strokeWidth={1} />,
163 link: ({ projectRef }) => `/project/${projectRef}/auth/providers`,
164 linkText: 'View settings',
165 docsLink: `${DOCS_URL}/guides/platform/going-into-prod#security`,
166 category: 'security',
167 },
168 {
169 name: 'auth_otp_short_length',
170 title: 'Auth OTP Short Length',
171 icon: <Ruler className="text-foreground-muted" size={15} strokeWidth={1} />,
172 link: ({ projectRef }) => `/project/${projectRef}/auth/providers`,
173 linkText: 'View settings',
174 docsLink: `${DOCS_URL}/guides/platform/going-into-prod#security`,
175 category: 'security',
176 },
177 {
178 name: 'auth_db_connections_absolute',
179 title: 'Auth Absolute Connection Management Strategy',
180 icon: <Scaling className="text-foreground-muted" size={15} strokeWidth={1} />,
181 link: ({ projectRef }) => `/project/${projectRef}/auth/performance`,
182 linkText: 'View settings',
183 docsLink: `${DOCS_URL}/guides/platform/going-into-prod`,
184 category: 'performance',
185 },
186 {
187 name: 'rls_references_user_metadata',
188 title: 'RLS references user metadata',
189 icon: <User className="text-foreground-muted" size={15} strokeWidth={1} />,
190 link: ({ projectRef }) => `/project/${projectRef}/auth/policies`,
191 linkText: 'View policies',
192 docsLink: `${DOCS_URL}/guides/database/database-linter?queryGroups=lint&lint=0015_rls_references_user_metadata`,
193 category: 'security',
194 },
195 {
196 name: 'materialized_view_in_api',
197 title: 'Materialized View in API',
198 icon: <Eye className="text-foreground-muted" size={15} strokeWidth={1.5} />,
199 link: () => `${DOCS_URL}/guides/database/database-advisors?lint=0016_materialized_view_in_api`,
200 linkText: 'View docs',
201 docsLink: `${DOCS_URL}/guides/database/database-advisors?lint=0016_materialized_view_in_api`,
202 category: 'security',
203 },
204 {
205 name: 'foreign_table_in_api',
206 title: 'Foreign Table in API',
207 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1.5} />,
208 link: () => `${DOCS_URL}/guides/database/database-linter?lint=0017_foreign_table_in_api`,
209 linkText: 'View docs',
210 docsLink: `${DOCS_URL}/guides/database/database-linter?lint=0017_foreign_table_in_api`,
211 category: 'security',
212 },
213 {
214 name: 'unsupported_reg_types',
215 title: 'Unsupported reg types',
216 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1.5} />,
217 link: () =>
218 `${DOCS_URL}/guides/database/database-advisors?lint=0018_unsupported_reg_types&queryGroups=lint`,
219 linkText: 'View docs',
220 docsLink: `${DOCS_URL}/guides/database/database-advisors?lint=0018_unsupported_reg_types&queryGroups=lint`,
221 category: 'security',
222 },
223 {
224 name: 'ssl_not_enforced',
225 title: 'SSL not enforced',
226 icon: <Ruler className="text-foreground-muted" size={15} strokeWidth={1} />,
227 link: ({ projectRef }) => `/project/${projectRef}/database/settings`,
228 linkText: 'View settings',
229 docsLink: `${DOCS_URL}/guides/platform/ssl-enforcement`,
230 category: 'security',
231 },
232 {
233 name: 'network_restrictions_not_set',
234 title: 'No network restrictions',
235 icon: <Ruler className="text-foreground-muted" size={15} strokeWidth={1} />,
236 link: ({ projectRef }) => `/project/${projectRef}/database/settings`,
237 linkText: 'View settings',
238 docsLink: `${DOCS_URL}/guides/platform/network-restrictions`,
239 category: 'security',
240 },
241 {
242 name: 'password_requirements_min_length',
243 title: 'Minimum password length not set or inadequate',
244 icon: <Ruler className="text-foreground-muted" size={15} strokeWidth={1} />,
245 link: ({ projectRef }) => `/project/${projectRef}/auth/providers?provider=Email`,
246 linkText: 'View settings',
247 docsLink: `${DOCS_URL}/guides/platform/going-into-prod#security`,
248 category: 'security',
249 },
250 {
251 name: 'pitr_not_enabled',
252 title: 'PITR not enabled',
253 icon: <Ruler className="text-foreground-muted" size={15} strokeWidth={1} />,
254 link: ({ projectRef }) => `/project/${projectRef}/database/backups/pitr`,
255 linkText: 'View settings',
256 docsLink: `${DOCS_URL}/guides/platform/backups#point-in-time-recovery`,
257 category: 'security',
258 },
259 {
260 name: 'auth_leaked_password_protection',
261 title: 'Leaked Password Protection Disabled',
262 icon: <LockIcon className="text-foreground-muted" size={15} strokeWidth={1} />,
263 link: ({ projectRef }) => `/project/${projectRef}/auth/providers?provider=Email`,
264 linkText: 'View settings',
265 docsLink: `${DOCS_URL}/guides/auth/password-security#password-strength-and-leaked-password-protection`,
266 category: 'security',
267 },
268 {
269 name: 'auth_insufficient_mfa_options',
270 title: 'Insufficient MFA Options',
271 icon: <LockIcon className="text-foreground-muted" size={15} strokeWidth={1} />,
272 link: ({ projectRef }) => `/project/${projectRef}/auth/mfa`,
273 linkText: 'View settings',
274 docsLink: `${DOCS_URL}/guides/auth/auth-mfa`,
275 category: 'security',
276 },
277 {
278 name: 'auth_password_policy_missing',
279 title: 'Password Policy Missing',
280 icon: <LockIcon className="text-foreground-muted" size={15} strokeWidth={1} />,
281 link: ({ projectRef }) => `/project/${projectRef}/auth/providers?provider=Email`,
282 linkText: 'View settings',
283 docsLink: `${DOCS_URL}/guides/auth/password-security`,
284 category: 'security',
285 },
286 {
287 name: 'leaked_service_key',
288 title: 'Leaked Service Key Detected',
289 icon: <LockIcon className="text-foreground-muted" size={15} strokeWidth={1} />,
290 link: ({ projectRef }) => `/project/${projectRef}/settings/api-keys`,
291 linkText: 'View settings',
292 docsLink: `${DOCS_URL}/guides/api/api-keys#the-servicerole-key`,
293 category: 'security',
294 },
295 {
296 name: 'no_backup_admin',
297 title: 'No Backup Admin Detected',
298 icon: <LockIcon className="text-foreground-muted" size={15} strokeWidth={1} />,
299 link: ({ projectRef }) => `/project/${projectRef}/auth/mfa`,
300 linkText: 'View settings',
301 docsLink: `${DOCS_URL}/guides/auth/auth-mfa`,
302 category: 'security',
303 },
304 {
305 name: 'vulnerable_postgres_version',
306 title: 'Postgres version has security patches available',
307 icon: <LockIcon className="text-foreground-muted" size={15} strokeWidth={1} />,
308 link: ({ projectRef }) => `/project/${projectRef}/settings/infrastructure`,
309 linkText: 'View settings',
310 docsLink: `${DOCS_URL}/guides/platform/upgrading`,
311 category: 'security',
312 },
313 {
314 name: 'sensitive_columns_exposed',
315 title: 'Sensitive Columns Exposed',
316 icon: <Eye className="text-foreground-muted" size={15} strokeWidth={1.5} />,
317 link: ({ projectRef, metadata }) =>
318 `/project/${projectRef}/editor?schema=${encodeURIComponent(metadata?.schema ?? '')}&table=${encodeURIComponent(metadata?.name ?? '')}`,
319 linkText: 'View table',
320 docsLink: `${DOCS_URL}/guides/database/database-linter?lint=0023_sensitive_columns_exposed`,
321 category: 'security',
322 },
323 {
324 name: 'rls_policy_always_true',
325 title: 'RLS Policy Always True',
326 icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
327 link: ({ projectRef, metadata }) =>
328 `/project/${projectRef}/auth/policies?schema=${encodeURIComponent(metadata?.schema ?? '')}&search=${encodeURIComponent(metadata?.name ?? '')}`,
329 linkText: 'View policies',
330 docsLink: `${DOCS_URL}/guides/database/database-linter?lint=0024_permissive_rls_policy`,
331 category: 'security',
332 },
333 {
334 name: 'public_bucket_allows_listing',
335 title: 'Public Bucket Allows Listing',
336 icon: <Box className="text-foreground-muted" size={15} strokeWidth={1.5} />,
337 link: ({ projectRef, metadata }) => {
338 const bucketId = (metadata as Record<string, string | undefined> | undefined)?.bucket_id
339 return `/project/${projectRef}/storage/files/buckets/${encodeURIComponent(bucketId ?? metadata?.name ?? '')}`
340 },
341 linkText: 'View bucket',
342 docsLink: `${DOCS_URL}/guides/database/database-linter?lint=0025_public_bucket_allows_listing`,
343 category: 'security',
344 },
345 {
346 name: 'pg_graphql_anon_table_exposed',
347 title: 'Public Can See Object in GraphQL Schema',
348 icon: <Eye className="text-foreground-muted" size={15} strokeWidth={1.5} />,
349 link: ({ projectRef, metadata }) =>
350 `/project/${projectRef}/editor?schema=${encodeURIComponent(metadata?.schema ?? '')}&table=${encodeURIComponent(metadata?.name ?? '')}`,
351 linkText: 'View object',
352 docsLink: `${DOCS_URL}/guides/database/database-linter?lint=0026_pg_graphql_anon_table_exposed`,
353 category: 'security',
354 },
355 {
356 name: 'pg_graphql_authenticated_table_exposed',
357 title: 'Signed-In Users Can See Object in GraphQL Schema',
358 icon: <Eye className="text-foreground-muted" size={15} strokeWidth={1.5} />,
359 link: ({ projectRef, metadata }) =>
360 `/project/${projectRef}/editor?schema=${encodeURIComponent(metadata?.schema ?? '')}&table=${encodeURIComponent(metadata?.name ?? '')}`,
361 linkText: 'View object',
362 docsLink: `${DOCS_URL}/guides/database/database-linter?lint=0027_pg_graphql_authenticated_table_exposed`,
363 category: 'security',
364 },
365 {
366 name: 'anon_security_definer_function_executable',
367 title: 'Public Can Execute SECURITY DEFINER Function',
368 icon: <LockIcon className="text-foreground-muted" size={15} strokeWidth={1} />,
369 link: ({ projectRef, metadata }) =>
370 `/project/${projectRef}/database/functions?schema=${encodeURIComponent(metadata?.schema ?? '')}&search=${encodeURIComponent(metadata?.name ?? '')}`,
371 linkText: 'View function',
372 docsLink: `${DOCS_URL}/guides/database/database-linter?lint=0028_anon_security_definer_function_executable`,
373 category: 'security',
374 },
375 {
376 name: 'authenticated_security_definer_function_executable',
377 title: 'Signed-In Users Can Execute SECURITY DEFINER Function',
378 icon: <LockIcon className="text-foreground-muted" size={15} strokeWidth={1} />,
379 link: ({ projectRef, metadata }) =>
380 `/project/${projectRef}/database/functions?schema=${encodeURIComponent(metadata?.schema ?? '')}&search=${encodeURIComponent(metadata?.name ?? '')}`,
381 linkText: 'View function',
382 docsLink: `${DOCS_URL}/guides/database/database-linter?lint=0029_authenticated_security_definer_function_executable`,
383 category: 'security',
384 },
385]
386
387export const LintCTA = ({
388 title,
389 projectRef,
390 metadata,
391 onAfterAction,
392}: {
393 title: LINT_TYPES
394 projectRef: string
395 metadata: Lint['metadata']
396 onAfterAction?: () => void
397}) => {
398 const lintInfo = lintInfoMap.find((item) => item.name === title)
399
400 if (!lintInfo) {
401 return null
402 }
403
404 const graphqlExposureLintName = asGraphqlExposureLint(title)
405 if (graphqlExposureLintName) {
406 return (
407 <GraphqlExposureLintCTA
408 lintName={graphqlExposureLintName}
409 projectRef={projectRef}
410 metadata={metadata}
411 onAfterAction={onAfterAction}
412 />
413 )
414 }
415
416 const link = lintInfo.link({ projectRef, metadata })
417 const linkText = lintInfo.linkText
418
419 return (
420 <Button asChild type="default">
421 <Link href={link} rel="noreferrer" className="no-underline">
422 {linkText}
423 </Link>
424 </Button>
425 )
426}
427
428export const EntityTypeIcon = ({ type }: { type: string | undefined }) => {
429 switch (type) {
430 case 'table':
431 return <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />
432 case 'view':
433 return <Eye className="text-foreground-muted" size={15} strokeWidth={1.5} />
434 case 'auth':
435 return <Lock className="text-foreground-muted" size={15} strokeWidth={1.5} />
436 default:
437 return <Box className="text-foreground-muted" size={15} strokeWidth={1.5} />
438 }
439}
440
441export const LintEntity = ({ metadata }: { metadata: Lint['metadata'] }) => {
442 return getLintEntityString(metadata)
443}
444
445export const LintCategoryBadge = ({ category }: { category: string }) => {
446 return (
447 <Badge variant={category === 'SECURITY' ? 'destructive' : 'warning'}>
448 {category.toLowerCase()}
449 </Badge>
450 )
451}
452
453export const NoIssuesFound = ({ level }: { level: string }) => {
454 const noun = level === LINTER_LEVELS.ERROR ? 'errors' : 'warnings'
455 return (
456 <div className="absolute top-28 px-6 flex flex-col items-center justify-center w-full gap-y-2">
457 <TextSearch className="text-foreground-muted" strokeWidth={1} />
458 <div className="text-center">
459 <p className="text-foreground">No {noun} detected</p>
460 <p className="text-foreground-light">
461 Congrats! There are no {noun} detected for this database
462 </p>
463 </div>
464 </div>
465 )
466}
467
468export const createLintSummaryPrompt = (lint: Lint) => {
469 const title = lintInfoMap.find((item) => item.name === lint.name)?.title ?? lint.title
470 const entity = getLintEntityString(lint.metadata) || 'N/A'
471 const schema = lint.metadata?.schema ?? 'N/A'
472 const issue = lint.detail ? lint.detail.replace(/\\`/g, '`') : 'N/A'
473 const description = lint.description ? lint.description.replace(/\\`/g, '`') : 'N/A'
474 return `Summarize the issue and suggest fixes for the following lint item:
475Title: ${title}
476Entity: ${entity}
477Schema: ${schema}
478Issue Details: ${issue}
479Description: ${description}`
480}
481
482export const getLintEntityString = (metadata: Lint['metadata']) => {
483 if (!metadata) {
484 return undefined
485 }
486
487 if (metadata.entity) {
488 return metadata.entity
489 }
490
491 if (metadata.schema && metadata.name) {
492 const extendedMetadata = metadata as typeof metadata & { arguments?: string }
493 const args =
494 typeof extendedMetadata.arguments === 'string' ? extendedMetadata.arguments : undefined
495 return `${metadata.schema}.${metadata.name}${args !== undefined ? `(${args})` : ''}`
496 }
497
498 return undefined
499}