From c50527b4c08e71492c3f50d4a41bdb95a98ba43a Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 29 Oct 2025 17:32:52 -0700 Subject: [PATCH] Security Panel MVP (#1660) TODOs: - [x] Add documentation - [x] e2e tests: run security review, update knowledge, and fix issue - [x] more stringent risk rating --- > [!NOTE] > Introduces a new Security mode with a Security Review panel that runs reviews, edits rules, parses findings via IPC, and supports fixing issues, with tests and prompt/runtime support. > > - **UI/Preview Panel**: > - Add `security` preview mode to `previewModeAtom` and ActionHeader (Shield button). > - New `SecurityPanel` showing findings table (sorted by severity), run review, fix issue flow, and edit `SECURITY_RULES.md` dialog. > - Wire into `PreviewPanel` content switch. > - **Hooks**: > - `useSecurityReview(appId)`: fetch latest review via IPC. > - `useStreamChat`: add `onSettled` callback to invoke refreshes after streams. > - **IPC/Main**: > - `security_handlers`: `get-latest-security-review` parses `` from latest assistant message. > - Register handler in `ipc_host`; expose channel in `preload`. > - `ipc_client`: add `getLatestSecurityReview(appId)`. > - `chat_stream_handlers`: detect `/security-review`, use dedicated system prompt, optionally append `SECURITY_RULES.md`, suppress Supabase-not-available note in this mode. > - **Prompts**: > - Add `SECURITY_REVIEW_SYSTEM_PROMPT` with structured finding output. > - **Supabase**: > - Enhance schema query to include `rls_enabled`, split policy `using_clause`/`with_check_clause`. > - **E2E Tests**: > - New `security_review.spec.ts` plus snapshots and fixture findings; update test helper for `security` mode and findings table snapshot. > - Fake LLM server streams security findings for `/security-review` and increases batch size. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5022d01e22a2dd929a968eeba0da592e0aeece01. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../fixtures/security-review/findings.md | 132 ++ e2e-tests/helpers/test_helper.ts | 10 +- e2e-tests/security_review.spec.ts | 45 + ...rity-review---edit-and-use-knowledge-1.txt | 1079 +++++++++++++++++ ..._review.spec.ts_security-review-1.aria.yml | 130 ++ ...urity_review.spec.ts_security-review-1.txt | 1070 ++++++++++++++++ ..._review.spec.ts_security-review-2.aria.yml | 45 + src/atoms/appAtoms.ts | 2 +- src/components/preview_panel/ActionHeader.tsx | 15 +- src/components/preview_panel/PreviewPanel.tsx | 3 + .../preview_panel/SecurityPanel.tsx | 811 +++++++++++++ src/hooks/useSecurityReview.ts | 20 + src/hooks/useStreamChat.ts | 5 + src/ipc/handlers/chat_stream_handlers.ts | 28 +- src/ipc/handlers/security_handlers.ts | 71 ++ src/ipc/ipc_client.ts | 7 + src/ipc/ipc_host.ts | 2 + src/ipc/ipc_types.ts | 12 + src/preload.ts | 1 + src/prompts/security_review_prompt.ts | 60 + src/supabase_admin/supabase_schema_query.ts | 11 +- .../fake-llm-server/chatCompletionHandler.ts | 24 +- 22 files changed, 3574 insertions(+), 9 deletions(-) create mode 100644 e2e-tests/fixtures/security-review/findings.md create mode 100644 e2e-tests/security_review.spec.ts create mode 100644 e2e-tests/snapshots/security_review.spec.ts_security-review---edit-and-use-knowledge-1.txt create mode 100644 e2e-tests/snapshots/security_review.spec.ts_security-review-1.aria.yml create mode 100644 e2e-tests/snapshots/security_review.spec.ts_security-review-1.txt create mode 100644 e2e-tests/snapshots/security_review.spec.ts_security-review-2.aria.yml create mode 100644 src/components/preview_panel/SecurityPanel.tsx create mode 100644 src/hooks/useSecurityReview.ts create mode 100644 src/ipc/handlers/security_handlers.ts create mode 100644 src/prompts/security_review_prompt.ts diff --git a/e2e-tests/fixtures/security-review/findings.md b/e2e-tests/fixtures/security-review/findings.md new file mode 100644 index 0000000..728241f --- /dev/null +++ b/e2e-tests/fixtures/security-review/findings.md @@ -0,0 +1,132 @@ +OK, let's review the security. + +Here are variations with different severity levels. + +Purposefully putting medium on top to make sure the severity levels are sorted correctly. + +## Medium Severity + + +**What**: The file upload endpoint accepts any file type without validating extensions or content, only checking file size + +**Risk**: An attacker could upload malicious files (e.g., .exe, .php) that might be executed if the server is misconfigured, or upload extremely large files to consume storage space + +**Potential Solutions**: +1. Implement a whitelist of allowed file extensions (e.g., `.jpg`, `.png`, `.pdf`) +2. Validate file content type using magic numbers, not just the extension +3. Store uploaded files outside the web root with random filenames +4. Implement virus scanning for uploaded files using ClamAV or similar + +**Relevant Files**: `src/api/upload.ts` + + + + +**What**: POST, PUT, and DELETE endpoints don't implement CSRF tokens, making them vulnerable to cross-site request forgery attacks + +**Risk**: An attacker could trick authenticated users into unknowingly performing actions like changing their email, making purchases, or deleting data by visiting a malicious website + +**Potential Solutions**: +1. Implement CSRF tokens using a library like `csurf` for Express +2. Set `SameSite=Strict` or `SameSite=Lax` on session cookies +3. Verify the `Origin` or `Referer` header for sensitive operations +4. For API-only applications, consider using custom headers that browsers can't set cross-origin + +**Relevant Files**: `src/middleware/auth.ts`, `src/api/*.ts` + + + +## Critical Severity + + +**What**: User input flows directly into database queries without validation, allowing attackers to execute arbitrary SQL commands + +**Risk**: An attacker could steal all customer data, delete your entire database, or take over admin accounts by manipulating the URL + +**Potential Solutions**: +1. Use parameterized queries: `db.query('SELECT * FROM users WHERE id = ?', [userId])` +2. Add input validation to ensure `userId` is a number +3. Implement an ORM like Prisma or TypeORM that prevents SQL injection by default + +**Relevant Files**: `src/api/users.ts` + + + + +**What**: AWS access keys are stored directly in the codebase and committed to version control, exposing full cloud infrastructure access + +**Risk**: Anyone with repository access (including former employees or compromised accounts) could spin up expensive resources, access S3 buckets with customer data, or destroy production infrastructure + +**Potential Solutions**: +1. Immediately rotate the exposed credentials in AWS IAM +2. Use environment variables and add `.env` to `.gitignore` +3. Implement AWS Secrets Manager or similar vault solution +4. Scan git history and purge the credentials using tools like `git-filter-repo` + +**Relevant Files**: `src/config/aws.ts`, `src/services/s3-uploader.ts` + + + +## High Severity + + +**What**: Administrative API endpoints can be accessed without authentication, relying only on URL obscurity + +**Risk**: An attacker who discovers these endpoints could modify user permissions, access sensitive reports, or change system configurations without credentials + +**Potential Solutions**: +1. Add authentication middleware to all `/admin/*` routes +2. Implement role-based access control (RBAC) to verify admin permissions +3. Add audit logging for all administrative actions +4. Consider implementing rate limiting on admin endpoints + +**Relevant Files**: `src/api/admin/users.ts`, `src/api/admin/settings.ts` + + + + +**What**: The application uses a hardcoded default JWT secret ("your-secret-key") for signing authentication tokens + +**Risk**: Attackers can forge valid JWT tokens to impersonate any user, including administrators, granting them unauthorized access to user accounts and sensitive data + +**Potential Solutions**: +1. Generate a strong random secret: `openssl rand -base64 32` +2. Store the secret in environment variables +3. Rotate the JWT secret, which will invalidate all existing sessions +4. Consider using RS256 (asymmetric) instead of HS256 for better security + +**Relevant Files**: `src/auth/jwt.ts` + + + +## Low Severity + + +**What**: Production error responses include full stack traces and internal file paths that are sent to end users + +**Risk**: Attackers can use this information to map your application structure, identify frameworks and versions, and find potential attack vectors more easily + +**Potential Solutions**: +1. Configure different error handlers for production vs development +2. Log detailed errors server-side but send generic messages to clients +3. Use an error handling middleware: `if (process.env.NODE_ENV === 'production') { /* hide details */ }` +4. Implement centralized error logging with tools like Sentry + +**Relevant Files**: `src/middleware/error-handler.ts` + + + + +**What**: The application doesn't set recommended security headers like `X-Frame-Options`, `X-Content-Type-Options`, and `Strict-Transport-Security` + +**Risk**: Users may be vulnerable to clickjacking attacks, MIME-type sniffing, or man-in-the-middle attacks, though exploitation requires specific conditions + +**Potential Solutions**: +1. Use Helmet.js middleware: `app.use(helmet())` +2. Configure headers manually in your web server (nginx/Apache) or application +3. Set `Content-Security-Policy` to prevent XSS attacks +4. Enable HSTS to enforce HTTPS connections + +**Relevant Files**: `src/app.ts`, `nginx.conf` + + \ No newline at end of file diff --git a/e2e-tests/helpers/test_helper.ts b/e2e-tests/helpers/test_helper.ts index 58cb305..11a8064 100644 --- a/e2e-tests/helpers/test_helper.ts +++ b/e2e-tests/helpers/test_helper.ts @@ -476,7 +476,9 @@ export class PageObject { // Preview panel //////////////////////////////// - async selectPreviewMode(mode: "code" | "problems" | "preview" | "configure") { + async selectPreviewMode( + mode: "code" | "problems" | "preview" | "configure" | "security", + ) { await this.page.getByTestId(`${mode}-mode-button`).click(); } @@ -596,6 +598,12 @@ export class PageObject { }); } + async snapshotSecurityFindingsTable() { + await expect( + this.page.getByTestId("security-findings-table"), + ).toMatchAriaSnapshot(); + } + async snapshotServerDump( type: "all-messages" | "last-message" | "request" = "all-messages", { name = "", dumpIndex = -1 }: { name?: string; dumpIndex?: number } = {}, diff --git a/e2e-tests/security_review.spec.ts b/e2e-tests/security_review.spec.ts new file mode 100644 index 0000000..9d1b0e2 --- /dev/null +++ b/e2e-tests/security_review.spec.ts @@ -0,0 +1,45 @@ +import { test, testSkipIfWindows } from "./helpers/test_helper"; + +// Skipping because snapshotting the security findings table is not +// consistent across platforms because different amounts of text +// get ellipsis'd out. +testSkipIfWindows("security review", async ({ po }) => { + await po.setUp({ autoApprove: true }); + await po.sendPrompt("tc=1"); + + await po.selectPreviewMode("security"); + + await po.page + .getByRole("button", { name: "Run Security Review" }) + .first() + .click(); + await po.waitForChatCompletion(); + await po.snapshotServerDump("all-messages"); + await po.snapshotSecurityFindingsTable(); + + await po.page.getByRole("button", { name: "Fix Issue" }).first().click(); + await po.waitForChatCompletion(); + await po.snapshotMessages(); +}); + +test("security review - edit and use knowledge", async ({ po }) => { + await po.setUp({ autoApprove: true }); + await po.sendPrompt("tc=1"); + + await po.selectPreviewMode("security"); + await po.page.getByRole("button", { name: "Edit Security Rules" }).click(); + await po.page + .getByRole("textbox", { name: "# SECURITY_RULES.md\\n\\" }) + .click(); + await po.page + .getByRole("textbox", { name: "# SECURITY_RULES.md\\n\\" }) + .fill("testing\nrules123"); + await po.page.getByRole("button", { name: "Save" }).click(); + + await po.page + .getByRole("button", { name: "Run Security Review" }) + .first() + .click(); + await po.waitForChatCompletion(); + await po.snapshotServerDump("all-messages"); +}); diff --git a/e2e-tests/snapshots/security_review.spec.ts_security-review---edit-and-use-knowledge-1.txt b/e2e-tests/snapshots/security_review.spec.ts_security-review---edit-and-use-knowledge-1.txt new file mode 100644 index 0000000..f52ac1d --- /dev/null +++ b/e2e-tests/snapshots/security_review.spec.ts_security-review---edit-and-use-knowledge-1.txt @@ -0,0 +1,1079 @@ +=== +role: system +message: +# Role +Security expert identifying vulnerabilities that could lead to data breaches, leaks, or unauthorized access. + +# Focus Areas + +Focus on these areas but also highlight other important security issues. + +## Authentication & Authorization +Authentication bypass, broken access controls, insecure sessions, JWT/OAuth flaws, privilege escalation + +## Injection Attacks +SQL injection, XSS (Cross-Site Scripting), command injection - focus on data exfiltration and credential theft + +## API Security +Unauthenticated endpoints, missing authorization, excessive data in responses, IDOR vulnerabilities + +## Client-Side Secrets +Private API keys/tokens exposed in browser where they can be stolen + +# Output Format + + +**What**: Plain-language explanation +**Risk**: Data exposure impact (e.g., "All customer emails could be stolen") +**Potential Solutions**: Options ranked by how effectively they address the issue +**Relevant Files**: Relevant file paths + + +# Example: + + +**What**: User input flows directly into database queries without validation, allowing attackers to execute arbitrary SQL commands + +**Risk**: An attacker could steal all customer data, delete your entire database, or take over admin accounts by manipulating the URL + +**Potential Solutions**: +1. Use parameterized queries: `db.query('SELECT * FROM users WHERE id = ?', [userId])` +2. Add input validation to ensure `userId` is a number +3. Implement an ORM like Prisma or TypeORM that prevents SQL injection by default + +**Relevant Files**: `src/api/users.ts` + + + +# Severity Levels +**critical**: Actively exploitable or trivially exploitable, leading to full system or data compromise with no mitigation in place. +**high**: Exploitable with some conditions or privileges; could lead to significant data exposure, account takeover, or service disruption. +**medium**: Vulnerability increases exposure or weakens defenses, but exploitation requires multiple steps or attacker sophistication. +**low**: Low immediate risk; typically requires local access, unlikely chain of events, or only violates best practices without a clear exploitation path. + +# Instructions +1. Find real, exploitable vulnerabilities that lead to data breaches +2. Prioritize client-side exposed secrets and data leaks +3. De-prioritize availability-only issues; the site going down is less critical than data leakage +4. Use plain language with specific file paths +5. Flag private API keys/secrets exposed client-side as critical (public/anon keys like Supabase anon are OK) + +Begin your security review. + + +# Project-specific security rules: +testing +rules123 + + +If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets), +tell them that they need to add supabase to their app. + +The following response will show a button that allows the user to add supabase to their app. + + + +# Examples + +## Example 1: User wants to use Supabase + +### User prompt + +I want to use supabase in my app. + +### Assistant response + +You need to first add Supabase to your app. + + + +## Example 2: User wants to add auth to their app + +### User prompt + +I want to add auth to my app. + +### Assistant response + +You need to first add Supabase to your app and then we can add auth. + + + + +=== +role: user +message: This is my codebase. +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + + + + +# Tech Stack + +- You are building a React application. +- Use TypeScript. +- Use React Router. KEEP the routes in src/App.tsx +- Always put source code in the src folder. +- Put pages into src/pages/ +- Put components into src/components/ +- The main page (default page) is src/pages/Index.tsx +- UPDATE the main page to include the new components. OTHERWISE, the user can NOT see any components! +- ALWAYS try to use the shadcn/ui library. +- Tailwind CSS: always use Tailwind CSS for styling components. Utilize Tailwind classes extensively for layout, spacing, colors, and other design aspects. + +Available packages and libraries: + +- The lucide-react package is installed for icons. +- You ALREADY have ALL the shadcn/ui components and their dependencies installed. So you don't need to install them again. +- You have ALL the necessary Radix UI components installed. +- Use prebuilt components from the shadcn/ui library after importing them. Note that these files shouldn't be edited, so make new components if you need to change them. + + + + +// File contents excluded from context + + + +// File contents excluded from context + + + + + + + + + dyad-generated-app + + + +
+ + + + +
+ + +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +# Welcome to your Dyad app + + + + +testing +rules123 + + + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + + + + +import { Toaster } from "@/components/ui/toaster"; +import { Toaster as Sonner } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Index from "./pages/Index"; +import NotFound from "./pages/NotFound"; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + + + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + +); + +export default App; + + + + +export const MadeWithDyad = () => { + return ( +
+ + Made with Dyad + +
+ ); +}; + +
+ + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + + --sidebar-background: 0 0% 98%; + + --sidebar-foreground: 240 5.3% 26.1%; + + --sidebar-primary: 240 5.9% 10%; + + --sidebar-primary-foreground: 0 0% 98%; + + --sidebar-accent: 240 4.8% 95.9%; + + --sidebar-accent-foreground: 240 5.9% 10%; + + --sidebar-border: 220 13% 91%; + + --sidebar-ring: 217.2 91.2% 59.8%; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } +} + + + + +import * as React from "react"; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState( + undefined, + ); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener("change", onChange); + }, []); + + return !!isMobile; +} + + + + +import * as React from "react"; + +import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; + +const TOAST_LIMIT = 1; +const TOAST_REMOVE_DELAY = 1000000; + +type ToasterToast = ToastProps & { + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; + +const _actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const; + +let count = 0; + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); +} + +type ActionType = typeof _actionTypes; + +type Action = + | { + type: ActionType["ADD_TOAST"]; + toast: ToasterToast; + } + | { + type: ActionType["UPDATE_TOAST"]; + toast: Partial; + } + | { + type: ActionType["DISMISS_TOAST"]; + toastId?: ToasterToast["id"]; + } + | { + type: ActionType["REMOVE_TOAST"]; + toastId?: ToasterToast["id"]; + }; + +interface State { + toasts: ToasterToast[]; +} + +const toastTimeouts = new Map>(); + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return; + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId); + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }); + }, TOAST_REMOVE_DELAY); + + toastTimeouts.set(toastId, timeout); +}; + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + }; + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t, + ), + }; + + case "DISMISS_TOAST": { + const { toastId } = action; + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId); + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id); + }); + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t, + ), + }; + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + }; + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + }; + } +}; + +const listeners: Array<(state: State) => void> = []; + +let memoryState: State = { toasts: [] }; + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action); + listeners.forEach((listener) => { + listener(memoryState); + }); +} + +type Toast = Omit; + +function toast({ ...props }: Toast) { + const id = genId(); + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }); + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss(); + }, + }, + }); + + return { + id: id, + dismiss, + update, + }; +} + +function useToast() { + const [state, setState] = React.useState(memoryState); + + React.useEffect(() => { + listeners.push(setState); + return () => { + const index = listeners.indexOf(setState); + if (index > -1) { + listeners.splice(index, 1); + } + }; + }, [state]); + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + }; +} + +export { useToast, toast }; + + + + +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + + + + +import { createRoot } from "react-dom/client"; +import App from "./App.tsx"; +import "./globals.css"; + +createRoot(document.getElementById("root")!).render(); + + + + +// Update this page (the content is just a fallback if you fail to update the page) + +import { MadeWithDyad } from "@/components/made-with-dyad"; + +const Index = () => { + return ( +
+
+

Welcome to Your Blank App

+

+ Start building your amazing project here! +

+
+ +
+ ); +}; + +export default Index; + +
+ + +import { useLocation } from "react-router-dom"; +import { useEffect } from "react"; + +const NotFound = () => { + const location = useLocation(); + + useEffect(() => { + console.error( + "404 Error: User attempted to access non-existent route:", + location.pathname, + ); + }, [location.pathname]); + + return ( +
+
+

404

+

Oops! Page not found

+ + Return to Home + +
+
+ ); +}; + +export default NotFound; + +
+ + +import { toast } from "sonner"; + +export const showSuccess = (message: string) => { + toast.success(message); +}; + +export const showError = (message: string) => { + toast.error(message); +}; + +export const showLoading = (message: string) => { + return toast.loading(message); +}; + +export const dismissToast = (toastId: string) => { + toast.dismiss(toastId); +}; + + + + +/// + + + + +import type { Config } from "tailwindcss"; + +export default { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + sidebar: { + DEFAULT: "hsl(var(--sidebar-background))", + foreground: "hsl(var(--sidebar-foreground))", + primary: "hsl(var(--sidebar-primary))", + "primary-foreground": "hsl(var(--sidebar-primary-foreground))", + accent: "hsl(var(--sidebar-accent))", + "accent-foreground": "hsl(var(--sidebar-accent-foreground))", + border: "hsl(var(--sidebar-border))", + ring: "hsl(var(--sidebar-ring))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { + height: "0", + }, + to: { + height: "var(--radix-accordion-content-height)", + }, + }, + "accordion-up": { + from: { + height: "var(--radix-accordion-content-height)", + }, + to: { + height: "0", + }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} satisfies Config; + + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} + + + + +import { defineConfig } from "vite"; +import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger"; +import react from "@vitejs/plugin-react-swc"; +import path from "path"; + +export default defineConfig(() => ({ + server: { + host: "::", + port: 8080, + }, + plugins: [dyadComponentTagger(), react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +})); + + + + + +=== +role: assistant +message: OK, got it. I'm ready to help + +=== +role: user +message: /security-review \ No newline at end of file diff --git a/e2e-tests/snapshots/security_review.spec.ts_security-review-1.aria.yml b/e2e-tests/snapshots/security_review.spec.ts_security-review-1.aria.yml new file mode 100644 index 0000000..773cb36 --- /dev/null +++ b/e2e-tests/snapshots/security_review.spec.ts_security-review-1.aria.yml @@ -0,0 +1,130 @@ +- table: + - rowgroup: + - row "Level Issue Action": + - cell "Level" + - cell "Issue" + - cell "Action" + - rowgroup: + - 'row "critical SQL Injection in User Lookup What: User input flows directly into database queries without validation, allowing attackers to execute arbitrary SQL commands Risk: An attac... Show more Fix Issue"': + - cell "critical": + - img + - 'cell "SQL Injection in User Lookup What: User input flows directly into database queries without validation, allowing attackers to execute arbitrary SQL commands Risk: An attac... Show more"': + - 'button "SQL Injection in User Lookup What: User input flows directly into database queries without validation, allowing attackers to execute arbitrary SQL commands Risk: An attac... Show more"': + - paragraph: + - strong: What + - text: ": User input flows directly into database queries without validation, allowing attackers to execute arbitrary SQL commands" + - paragraph: + - strong: Risk + - text: ": An attac..." + - button "Show more": + - img + - cell "Fix Issue": + - button "Fix Issue" + - 'row "critical Hardcoded AWS Credentials in Source Code What: AWS access keys are stored directly in the codebase and committed to version control, exposing full cloud infrastructure access Risk: A... Show more Fix Issue"': + - cell "critical": + - img + - 'cell "Hardcoded AWS Credentials in Source Code What: AWS access keys are stored directly in the codebase and committed to version control, exposing full cloud infrastructure access Risk: A... Show more"': + - 'button "Hardcoded AWS Credentials in Source Code What: AWS access keys are stored directly in the codebase and committed to version control, exposing full cloud infrastructure access Risk: A... Show more"': + - paragraph: + - strong: What + - text: ": AWS access keys are stored directly in the codebase and committed to version control, exposing full cloud infrastructure access" + - paragraph: + - strong: Risk + - text: ": A..." + - button "Show more": + - img + - cell "Fix Issue": + - button "Fix Issue" + - 'row "high Missing Authentication on Admin Endpoints What: Administrative API endpoints can be accessed without authentication, relying only on URL obscurity Risk: An attacker who discovers thes... Show more Fix Issue"': + - cell "high": + - img + - 'cell "Missing Authentication on Admin Endpoints What: Administrative API endpoints can be accessed without authentication, relying only on URL obscurity Risk: An attacker who discovers thes... Show more"': + - 'button "Missing Authentication on Admin Endpoints What: Administrative API endpoints can be accessed without authentication, relying only on URL obscurity Risk: An attacker who discovers thes... Show more"': + - paragraph: + - strong: What + - text: ": Administrative API endpoints can be accessed without authentication, relying only on URL obscurity" + - paragraph: + - strong: Risk + - text: ": An attacker who discovers thes..." + - button "Show more": + - img + - cell "Fix Issue": + - button "Fix Issue" + - 'row "high JWT Secret Using Default Value What: The application uses a hardcoded default JWT secret (\"your-secret-key\") for signing authentication tokens Risk: Attackers can forge val... Show more Fix Issue"': + - cell "high": + - img + - 'cell "JWT Secret Using Default Value What: The application uses a hardcoded default JWT secret (\"your-secret-key\") for signing authentication tokens Risk: Attackers can forge val... Show more"': + - 'button "JWT Secret Using Default Value What: The application uses a hardcoded default JWT secret (\"your-secret-key\") for signing authentication tokens Risk: Attackers can forge val... Show more"': + - paragraph: + - strong: What + - text: ": The application uses a hardcoded default JWT secret (\"your-secret-key\") for signing authentication tokens" + - paragraph: + - strong: Risk + - text: ": Attackers can forge val..." + - button "Show more": + - img + - cell "Fix Issue": + - button "Fix Issue" + - 'row "medium Unvalidated File Upload Extensions What: The file upload endpoint accepts any file type without validating extensions or content, only checking file size Risk: An attacker coul... Show more Fix Issue"': + - cell "medium": + - img + - 'cell "Unvalidated File Upload Extensions What: The file upload endpoint accepts any file type without validating extensions or content, only checking file size Risk: An attacker coul... Show more"': + - 'button "Unvalidated File Upload Extensions What: The file upload endpoint accepts any file type without validating extensions or content, only checking file size Risk: An attacker coul... Show more"': + - paragraph: + - strong: What + - text: ": The file upload endpoint accepts any file type without validating extensions or content, only checking file size" + - paragraph: + - strong: Risk + - text: ": An attacker coul..." + - button "Show more": + - img + - cell "Fix Issue": + - button "Fix Issue" + - 'row "medium Missing CSRF Protection on State-Changing Operations What: POST, PUT, and DELETE endpoints don''t implement CSRF tokens, making them vulnerable to cross-site request forgery attacks Risk: An atta... Show more Fix Issue"': + - cell "medium": + - img + - 'cell "Missing CSRF Protection on State-Changing Operations What: POST, PUT, and DELETE endpoints don''t implement CSRF tokens, making them vulnerable to cross-site request forgery attacks Risk: An atta... Show more"': + - 'button "Missing CSRF Protection on State-Changing Operations What: POST, PUT, and DELETE endpoints don''t implement CSRF tokens, making them vulnerable to cross-site request forgery attacks Risk: An atta... Show more"': + - paragraph: + - strong: What + - text: ": POST, PUT, and DELETE endpoints don't implement CSRF tokens, making them vulnerable to cross-site request forgery attacks" + - paragraph: + - strong: Risk + - text: ": An atta..." + - button "Show more": + - img + - cell "Fix Issue": + - button "Fix Issue" + - 'row "low Verbose Error Messages Expose Stack Traces What: Production error responses include full stack traces and internal file paths that are sent to end users Risk: Attackers can use this in... Show more Fix Issue"': + - cell "low": + - img + - 'cell "Verbose Error Messages Expose Stack Traces What: Production error responses include full stack traces and internal file paths that are sent to end users Risk: Attackers can use this in... Show more"': + - 'button "Verbose Error Messages Expose Stack Traces What: Production error responses include full stack traces and internal file paths that are sent to end users Risk: Attackers can use this in... Show more"': + - paragraph: + - strong: What + - text: ": Production error responses include full stack traces and internal file paths that are sent to end users" + - paragraph: + - strong: Risk + - text: ": Attackers can use this in..." + - button "Show more": + - img + - cell "Fix Issue": + - button "Fix Issue" + - 'row "low Missing Security Headers What: The application doesn''t set recommended security headers like `X-Frame-Options`, `X-Content-Type-Options`, and `Strict-Transport-Security` ... Show more Fix Issue"': + - cell "low": + - img + - 'cell "Missing Security Headers What: The application doesn''t set recommended security headers like `X-Frame-Options`, `X-Content-Type-Options`, and `Strict-Transport-Security` ... Show more"': + - 'button "Missing Security Headers What: The application doesn''t set recommended security headers like `X-Frame-Options`, `X-Content-Type-Options`, and `Strict-Transport-Security` ... Show more"': + - paragraph: + - strong: What + - text: ": The application doesn't set recommended security headers like" + - code: "`X-Frame-Options`" + - text: "," + - code: "`X-Content-Type-Options`" + - text: ", and" + - code: "`Strict-Transport-Security`" + - paragraph: ... + - button "Show more": + - img + - cell "Fix Issue": + - button "Fix Issue" \ No newline at end of file diff --git a/e2e-tests/snapshots/security_review.spec.ts_security-review-1.txt b/e2e-tests/snapshots/security_review.spec.ts_security-review-1.txt new file mode 100644 index 0000000..c80a70d --- /dev/null +++ b/e2e-tests/snapshots/security_review.spec.ts_security-review-1.txt @@ -0,0 +1,1070 @@ +=== +role: system +message: +# Role +Security expert identifying vulnerabilities that could lead to data breaches, leaks, or unauthorized access. + +# Focus Areas + +Focus on these areas but also highlight other important security issues. + +## Authentication & Authorization +Authentication bypass, broken access controls, insecure sessions, JWT/OAuth flaws, privilege escalation + +## Injection Attacks +SQL injection, XSS (Cross-Site Scripting), command injection - focus on data exfiltration and credential theft + +## API Security +Unauthenticated endpoints, missing authorization, excessive data in responses, IDOR vulnerabilities + +## Client-Side Secrets +Private API keys/tokens exposed in browser where they can be stolen + +# Output Format + + +**What**: Plain-language explanation +**Risk**: Data exposure impact (e.g., "All customer emails could be stolen") +**Potential Solutions**: Options ranked by how effectively they address the issue +**Relevant Files**: Relevant file paths + + +# Example: + + +**What**: User input flows directly into database queries without validation, allowing attackers to execute arbitrary SQL commands + +**Risk**: An attacker could steal all customer data, delete your entire database, or take over admin accounts by manipulating the URL + +**Potential Solutions**: +1. Use parameterized queries: `db.query('SELECT * FROM users WHERE id = ?', [userId])` +2. Add input validation to ensure `userId` is a number +3. Implement an ORM like Prisma or TypeORM that prevents SQL injection by default + +**Relevant Files**: `src/api/users.ts` + + + +# Severity Levels +**critical**: Actively exploitable or trivially exploitable, leading to full system or data compromise with no mitigation in place. +**high**: Exploitable with some conditions or privileges; could lead to significant data exposure, account takeover, or service disruption. +**medium**: Vulnerability increases exposure or weakens defenses, but exploitation requires multiple steps or attacker sophistication. +**low**: Low immediate risk; typically requires local access, unlikely chain of events, or only violates best practices without a clear exploitation path. + +# Instructions +1. Find real, exploitable vulnerabilities that lead to data breaches +2. Prioritize client-side exposed secrets and data leaks +3. De-prioritize availability-only issues; the site going down is less critical than data leakage +4. Use plain language with specific file paths +5. Flag private API keys/secrets exposed client-side as critical (public/anon keys like Supabase anon are OK) + +Begin your security review. + + + +If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets), +tell them that they need to add supabase to their app. + +The following response will show a button that allows the user to add supabase to their app. + + + +# Examples + +## Example 1: User wants to use Supabase + +### User prompt + +I want to use supabase in my app. + +### Assistant response + +You need to first add Supabase to your app. + + + +## Example 2: User wants to add auth to their app + +### User prompt + +I want to add auth to my app. + +### Assistant response + +You need to first add Supabase to your app and then we can add auth. + + + + +=== +role: user +message: This is my codebase. +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + + + + +# Tech Stack + +- You are building a React application. +- Use TypeScript. +- Use React Router. KEEP the routes in src/App.tsx +- Always put source code in the src folder. +- Put pages into src/pages/ +- Put components into src/components/ +- The main page (default page) is src/pages/Index.tsx +- UPDATE the main page to include the new components. OTHERWISE, the user can NOT see any components! +- ALWAYS try to use the shadcn/ui library. +- Tailwind CSS: always use Tailwind CSS for styling components. Utilize Tailwind classes extensively for layout, spacing, colors, and other design aspects. + +Available packages and libraries: + +- The lucide-react package is installed for icons. +- You ALREADY have ALL the shadcn/ui components and their dependencies installed. So you don't need to install them again. +- You have ALL the necessary Radix UI components installed. +- Use prebuilt components from the shadcn/ui library after importing them. Note that these files shouldn't be edited, so make new components if you need to change them. + + + + +// File contents excluded from context + + + +// File contents excluded from context + + + + + + + + + dyad-generated-app + + + +
+ + + + +
+ + +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +# Welcome to your Dyad app + + + + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + + + + +import { Toaster } from "@/components/ui/toaster"; +import { Toaster as Sonner } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Index from "./pages/Index"; +import NotFound from "./pages/NotFound"; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + + + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + +); + +export default App; + + + + +export const MadeWithDyad = () => { + return ( +
+ + Made with Dyad + +
+ ); +}; + +
+ + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + + --sidebar-background: 0 0% 98%; + + --sidebar-foreground: 240 5.3% 26.1%; + + --sidebar-primary: 240 5.9% 10%; + + --sidebar-primary-foreground: 0 0% 98%; + + --sidebar-accent: 240 4.8% 95.9%; + + --sidebar-accent-foreground: 240 5.9% 10%; + + --sidebar-border: 220 13% 91%; + + --sidebar-ring: 217.2 91.2% 59.8%; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } +} + + + + +import * as React from "react"; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState( + undefined, + ); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener("change", onChange); + }, []); + + return !!isMobile; +} + + + + +import * as React from "react"; + +import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; + +const TOAST_LIMIT = 1; +const TOAST_REMOVE_DELAY = 1000000; + +type ToasterToast = ToastProps & { + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; + +const _actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const; + +let count = 0; + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); +} + +type ActionType = typeof _actionTypes; + +type Action = + | { + type: ActionType["ADD_TOAST"]; + toast: ToasterToast; + } + | { + type: ActionType["UPDATE_TOAST"]; + toast: Partial; + } + | { + type: ActionType["DISMISS_TOAST"]; + toastId?: ToasterToast["id"]; + } + | { + type: ActionType["REMOVE_TOAST"]; + toastId?: ToasterToast["id"]; + }; + +interface State { + toasts: ToasterToast[]; +} + +const toastTimeouts = new Map>(); + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return; + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId); + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }); + }, TOAST_REMOVE_DELAY); + + toastTimeouts.set(toastId, timeout); +}; + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + }; + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t, + ), + }; + + case "DISMISS_TOAST": { + const { toastId } = action; + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId); + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id); + }); + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t, + ), + }; + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + }; + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + }; + } +}; + +const listeners: Array<(state: State) => void> = []; + +let memoryState: State = { toasts: [] }; + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action); + listeners.forEach((listener) => { + listener(memoryState); + }); +} + +type Toast = Omit; + +function toast({ ...props }: Toast) { + const id = genId(); + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }); + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss(); + }, + }, + }); + + return { + id: id, + dismiss, + update, + }; +} + +function useToast() { + const [state, setState] = React.useState(memoryState); + + React.useEffect(() => { + listeners.push(setState); + return () => { + const index = listeners.indexOf(setState); + if (index > -1) { + listeners.splice(index, 1); + } + }; + }, [state]); + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + }; +} + +export { useToast, toast }; + + + + +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + + + + +import { createRoot } from "react-dom/client"; +import App from "./App.tsx"; +import "./globals.css"; + +createRoot(document.getElementById("root")!).render(); + + + + +// Update this page (the content is just a fallback if you fail to update the page) + +import { MadeWithDyad } from "@/components/made-with-dyad"; + +const Index = () => { + return ( +
+
+

Welcome to Your Blank App

+

+ Start building your amazing project here! +

+
+ +
+ ); +}; + +export default Index; + +
+ + +import { useLocation } from "react-router-dom"; +import { useEffect } from "react"; + +const NotFound = () => { + const location = useLocation(); + + useEffect(() => { + console.error( + "404 Error: User attempted to access non-existent route:", + location.pathname, + ); + }, [location.pathname]); + + return ( +
+
+

404

+

Oops! Page not found

+ + Return to Home + +
+
+ ); +}; + +export default NotFound; + +
+ + +import { toast } from "sonner"; + +export const showSuccess = (message: string) => { + toast.success(message); +}; + +export const showError = (message: string) => { + toast.error(message); +}; + +export const showLoading = (message: string) => { + return toast.loading(message); +}; + +export const dismissToast = (toastId: string) => { + toast.dismiss(toastId); +}; + + + + +/// + + + + +import type { Config } from "tailwindcss"; + +export default { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + sidebar: { + DEFAULT: "hsl(var(--sidebar-background))", + foreground: "hsl(var(--sidebar-foreground))", + primary: "hsl(var(--sidebar-primary))", + "primary-foreground": "hsl(var(--sidebar-primary-foreground))", + accent: "hsl(var(--sidebar-accent))", + "accent-foreground": "hsl(var(--sidebar-accent-foreground))", + border: "hsl(var(--sidebar-border))", + ring: "hsl(var(--sidebar-ring))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { + height: "0", + }, + to: { + height: "var(--radix-accordion-content-height)", + }, + }, + "accordion-up": { + from: { + height: "var(--radix-accordion-content-height)", + }, + to: { + height: "0", + }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} satisfies Config; + + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +// File contents excluded from context + + + +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} + + + + +import { defineConfig } from "vite"; +import dyadComponentTagger from "@dyad-sh/react-vite-component-tagger"; +import react from "@vitejs/plugin-react-swc"; +import path from "path"; + +export default defineConfig(() => ({ + server: { + host: "::", + port: 8080, + }, + plugins: [dyadComponentTagger(), react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +})); + + + + + +=== +role: assistant +message: OK, got it. I'm ready to help + +=== +role: user +message: /security-review \ No newline at end of file diff --git a/e2e-tests/snapshots/security_review.spec.ts_security-review-2.aria.yml b/e2e-tests/snapshots/security_review.spec.ts_security-review-2.aria.yml new file mode 100644 index 0000000..6ec6866 --- /dev/null +++ b/e2e-tests/snapshots/security_review.spec.ts_security-review-2.aria.yml @@ -0,0 +1,45 @@ +- paragraph: "Please fix the following security issue in a simple and effective way:" +- paragraph: + - strong: SQL Injection in User Lookup + - text: (critical severity) +- paragraph: + - strong: What + - text: ": User input flows directly into database queries without validation, allowing attackers to execute arbitrary SQL commands" +- paragraph: + - strong: Risk + - text: ": An attacker could steal all customer data, delete your entire database, or take over admin accounts by manipulating the URL" +- paragraph: + - strong: Potential Solutions + - text: ":" +- list: + - listitem: + - text: "Use parameterized queries:" + - code: "`db.query('SELECT * FROM users WHERE id = ?', [userId])`" + - listitem: + - text: Add input validation to ensure + - code: "`userId`" + - text: is a number + - listitem: Implement an ORM like Prisma or TypeORM that prevents SQL injection by default +- paragraph: + - strong: Relevant Files + - text: ":" + - code: "`src/api/users.ts`" +- img +- text: file1.txt +- button "Edit": + - img +- img +- text: file1.txt +- paragraph: More EOM +- button: + - img +- img +- text: Approved +- img +- text: less than a minute ago +- img +- text: wrote 1 file(s) +- button "Undo": + - img +- button "Retry": + - img \ No newline at end of file diff --git a/src/atoms/appAtoms.ts b/src/atoms/appAtoms.ts index 9a692b8..ed1e9f0 100644 --- a/src/atoms/appAtoms.ts +++ b/src/atoms/appAtoms.ts @@ -8,7 +8,7 @@ export const appsListAtom = atom([]); export const appBasePathAtom = atom(""); export const versionsListAtom = atom([]); export const previewModeAtom = atom< - "preview" | "code" | "problems" | "configure" | "publish" + "preview" | "code" | "problems" | "configure" | "publish" | "security" >("preview"); export const selectedVersionIdAtom = atom(null); export const appOutputAtom = atom([]); diff --git a/src/components/preview_panel/ActionHeader.tsx b/src/components/preview_panel/ActionHeader.tsx index 7694e6f..5cf8ea7 100644 --- a/src/components/preview_panel/ActionHeader.tsx +++ b/src/components/preview_panel/ActionHeader.tsx @@ -11,6 +11,7 @@ import { AlertTriangle, Wrench, Globe, + Shield, } from "lucide-react"; import { ChatActivityButton } from "@/components/chat/ChatActivity"; import { motion } from "framer-motion"; @@ -39,7 +40,8 @@ export type PreviewMode = | "code" | "problems" | "configure" - | "publish"; + | "publish" + | "security"; // Preview Header component with preview mode toggle export const ActionHeader = () => { @@ -51,6 +53,7 @@ export const ActionHeader = () => { const problemsRef = useRef(null); const configureRef = useRef(null); const publishRef = useRef(null); + const securityRef = useRef(null); const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 }); const [windowWidth, setWindowWidth] = useState(window.innerWidth); const { problemReport } = useCheckProblems(selectedAppId); @@ -136,6 +139,9 @@ export const ActionHeader = () => { case "publish": targetRef = publishRef; break; + case "security": + targetRef = securityRef; + break; default: return; } @@ -250,6 +256,13 @@ export const ActionHeader = () => { "Configure", "configure-mode-button", )} + {renderButton( + "security", + securityRef, + , + "Security", + "security-mode-button", + )} {renderButton( "publish", publishRef, diff --git a/src/components/preview_panel/PreviewPanel.tsx b/src/components/preview_panel/PreviewPanel.tsx index 42297f8..bd4011f 100644 --- a/src/components/preview_panel/PreviewPanel.tsx +++ b/src/components/preview_panel/PreviewPanel.tsx @@ -16,6 +16,7 @@ import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels"; import { Console } from "./Console"; import { useRunApp } from "@/hooks/useRunApp"; import { PublishPanel } from "./PublishPanel"; +import { SecurityPanel } from "./SecurityPanel"; interface ConsoleHeaderProps { isOpen: boolean; @@ -119,6 +120,8 @@ export function PreviewPanel() { ) : previewMode === "publish" ? ( + ) : previewMode === "security" ? ( + ) : ( )} diff --git a/src/components/preview_panel/SecurityPanel.tsx b/src/components/preview_panel/SecurityPanel.tsx new file mode 100644 index 0000000..ecad7a4 --- /dev/null +++ b/src/components/preview_panel/SecurityPanel.tsx @@ -0,0 +1,811 @@ +import { useAtomValue, useSetAtom } from "jotai"; +import { selectedAppIdAtom } from "@/atoms/appAtoms"; +import { selectedChatIdAtom } from "@/atoms/chatAtoms"; +import { useSecurityReview } from "@/hooks/useSecurityReview"; +import { IpcClient } from "@/ipc/ipc_client"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogClose, +} from "@/components/ui/dialog"; +import { + Shield, + AlertTriangle, + AlertCircle, + Info, + ChevronDown, + Pencil, +} from "lucide-react"; +import { useNavigate } from "@tanstack/react-router"; +import { useStreamChat } from "@/hooks/useStreamChat"; +import { showError } from "@/lib/toast"; +import { Badge } from "@/components/ui/badge"; +import type { SecurityFinding, SecurityReviewResult } from "@/ipc/ipc_types"; +import { useState, useEffect } from "react"; +import { VanillaMarkdownParser } from "@/components/chat/DyadMarkdownParser"; +import { showSuccess, showWarning } from "@/lib/toast"; +import { useLoadAppFile } from "@/hooks/useLoadAppFile"; +import { useQueryClient } from "@tanstack/react-query"; + +const getSeverityColor = (level: SecurityFinding["level"]) => { + switch (level) { + case "critical": + return "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 border-red-200 dark:border-red-800"; + case "high": + return "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300 border-orange-200 dark:border-orange-800"; + case "medium": + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800"; + case "low": + return "bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300 border-gray-200 dark:border-gray-800"; + } +}; + +const getSeverityIcon = (level: SecurityFinding["level"]) => { + switch (level) { + case "critical": + return ; + case "high": + return ; + case "medium": + return ; + case "low": + return ; + } +}; + +const DESCRIPTION_PREVIEW_LENGTH = 150; + +const createFindingKey = (finding: { + title: string; + level: string; + description: string; +}): string => { + return JSON.stringify({ + title: finding.title, + level: finding.level, + description: finding.description, + }); +}; + +const formatTimeAgo = (input: string | number | Date): string => { + const timestampMs = new Date(input).getTime(); + const nowMs = Date.now(); + const diffMs = Math.max(0, nowMs - timestampMs); + + const minutes = Math.floor(diffMs / 60000); + if (minutes < 1) return "just now"; + if (minutes < 60) { + return `${minutes} minute${minutes === 1 ? "" : "s"} ago`; + } + + const hours = Math.floor(minutes / 60); + if (hours < 24) { + return `${hours} hour${hours === 1 ? "" : "s"} ago`; + } + + const days = Math.floor(hours / 24); + return `${days} day${days === 1 ? "" : "s"} ago`; +}; + +const getSeverityOrder = (level: SecurityFinding["level"]): number => { + switch (level) { + case "critical": + return 0; + case "high": + return 1; + case "medium": + return 2; + case "low": + return 3; + default: + return 4; + } +}; + +function SeverityBadge({ level }: { level: SecurityFinding["level"] }) { + return ( + + {getSeverityIcon(level)} + {level} + + ); +} + +function RunReviewButton({ + isRunning, + onRun, +}: { + isRunning: boolean; + onRun: () => void; +}) { + return ( + + ); +} + +function ReviewSummary({ data }: { data: SecurityReviewResult }) { + const counts = data.findings.reduce( + (acc, finding) => { + acc[finding.level] = (acc[finding.level] || 0) + 1; + return acc; + }, + {} as Record, + ); + + const severityLevels: Array = [ + "critical", + "high", + "medium", + "low", + ]; + + return ( +
+
+ Last reviewed {formatTimeAgo(data.timestamp)} +
+
+ {severityLevels + .filter((level) => counts[level] > 0) + .map((level) => ( + + {getSeverityIcon(level)} + + {counts[level]} + + + {level} + + + ))} +
+
+ ); +} + +function SecurityHeader({ + isRunning, + onRun, + data, + onOpenEditRules, +}: { + isRunning: boolean; + onRun: () => void; + data?: SecurityReviewResult | undefined; + onOpenEditRules: () => void; +}) { + return ( +
+
+
+

+ + Security Review + + experimental + +

+ + {data && data.findings.length > 0 && } +
+
+ + +
+
+
+ ); +} + +function LoadingView() { + return ( +
+
+ + + + +
+

+ Loading... +

+
+ ); +} + +function NoAppSelectedView() { + return ( +
+
+ +
+

+ No App Selected +

+

+ Select an app to run a security review +

+
+ ); +} + +function RunningReviewCard() { + return ( + + +
+
+ + + + +
+

+ Security review is running +

+

+ Results will be available soon. +

+
+
+
+ ); +} + +function NoReviewCard({ + isRunning, + onRun, +}: { + isRunning: boolean; + onRun: () => void; +}) { + return ( + + +
+
+ +
+

+ No Security Review Found +

+

+ Run a security review to identify potential vulnerabilities in your + application. +

+ +
+
+
+ ); +} + +function NoIssuesCard({ data }: { data?: SecurityReviewResult }) { + return ( + + +
+
+ +
+

+ No Security Issues Found +

+

+ Your application passed the security review with no issues detected. +

+ {data && ( +

+ Last reviewed {formatTimeAgo(data.timestamp)} +

+ )} +
+
+
+ ); +} + +function FindingsTable({ + findings, + onOpenDetails, + onFix, + fixingFindingKey, +}: { + findings: SecurityFinding[]; + onOpenDetails: (finding: SecurityFinding) => void; + onFix: (finding: SecurityFinding) => void; + fixingFindingKey?: string | null; +}) { + return ( +
+ + + + + + + + + + {[...findings] + .sort( + (a, b) => getSeverityOrder(a.level) - getSeverityOrder(b.level), + ) + .map((finding, index) => { + const isLongDescription = + finding.description.length > DESCRIPTION_PREVIEW_LENGTH; + const displayDescription = isLongDescription + ? finding.description.substring(0, DESCRIPTION_PREVIEW_LENGTH) + + "..." + : finding.description; + const findingKey = createFindingKey(finding); + const isFixing = fixingFindingKey === findingKey; + + return ( + + + + + + ); + })} + +
+ Level + + Issue + + Action +
+ + +
onOpenDetails(finding)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onOpenDetails(finding); + } + }} + > +
+ {finding.title} +
+
+ +
+ {isLongDescription && ( + + )} +
+
+ +
+
+ ); +} + +function FindingDetailsDialog({ + open, + finding, + onClose, + onFix, + fixingFindingKey, +}: { + open: boolean; + finding: SecurityFinding | null; + onClose: (open: boolean) => void; + onFix: (finding: SecurityFinding) => void; + fixingFindingKey?: string | null; +}) { + return ( + + + + + {finding?.title} + {finding && } + + +
+ {finding && } +
+ + + + + + +
+
+ ); +} + +export const SecurityPanel = () => { + const selectedAppId = useAtomValue(selectedAppIdAtom); + const setSelectedChatId = useSetAtom(selectedChatIdAtom); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const { streamMessage } = useStreamChat({ hasChatId: false }); + const { data, isLoading, error, refetch } = useSecurityReview(selectedAppId); + const [isRunningReview, setIsRunningReview] = useState(false); + const [detailsOpen, setDetailsOpen] = useState(false); + const [detailsFinding, setDetailsFinding] = useState( + null, + ); + const [isEditRulesOpen, setIsEditRulesOpen] = useState(false); + const [rulesContent, setRulesContent] = useState(""); + const [fixingFindingKey, setFixingFindingKey] = useState(null); + const [isSaving, setIsSaving] = useState(false); + + const { + content: fetchedRules, + loading: isFetchingRules, + refreshFile: refetchRules, + } = useLoadAppFile( + isEditRulesOpen && selectedAppId ? selectedAppId : null, + isEditRulesOpen ? "SECURITY_RULES.md" : null, + ); + + useEffect(() => { + if (fetchedRules !== null) { + setRulesContent(fetchedRules); + } + }, [fetchedRules]); + + const handleSaveRules = async () => { + if (!selectedAppId) { + showError("No app selected"); + return; + } + + try { + setIsSaving(true); + const ipcClient = IpcClient.getInstance(); + const { warning } = await ipcClient.editAppFile( + selectedAppId, + "SECURITY_RULES.md", + rulesContent, + ); + await queryClient.invalidateQueries({ + queryKey: ["versions", selectedAppId], + }); + if (warning) { + showWarning(warning); + } else { + showSuccess("Security rules saved"); + } + setIsEditRulesOpen(false); + refetchRules(); + } catch (err: any) { + showError(`Failed to save security rules: ${err.message || err}`); + } finally { + setIsSaving(false); + } + }; + + const openFindingDetails = (finding: SecurityFinding) => { + setDetailsFinding(finding); + setDetailsOpen(true); + }; + + const handleRunSecurityReview = async () => { + if (!selectedAppId) { + showError("No app selected"); + return; + } + + try { + setIsRunningReview(true); + + // Create a new chat + const chatId = await IpcClient.getInstance().createChat(selectedAppId); + + // Navigate to the new chat + setSelectedChatId(chatId); + await navigate({ to: "/chat", search: { id: chatId } }); + + // Stream the security review prompt + await streamMessage({ + prompt: "/security-review", + chatId, + onSettled: () => { + refetch(); + setIsRunningReview(false); + }, + }); + } catch (err) { + showError(`Failed to run security review: ${err}`); + setIsRunningReview(false); + } + }; + + const handleFixIssue = async (finding: SecurityFinding) => { + if (!selectedAppId) { + showError("No app selected"); + return; + } + + try { + const key = createFindingKey(finding); + setFixingFindingKey(key); + + const chatId = await IpcClient.getInstance().createChat(selectedAppId); + + // Navigate to the new chat + setSelectedChatId(chatId); + await navigate({ to: "/chat", search: { id: chatId } }); + + const prompt = `Please fix the following security issue in a simple and effective way: + +**${finding.title}** (${finding.level} severity) + +${finding.description}`; + + await streamMessage({ + prompt, + chatId, + onSettled: () => { + setFixingFindingKey(null); + }, + }); + } catch (err) { + showError(`Failed to create fix chat: ${err}`); + setFixingFindingKey(null); + } + }; + + if (isLoading) { + return ; + } + + if (!selectedAppId) { + return ; + } + + return ( +
+
+ { + setIsEditRulesOpen(true); + if (selectedAppId) { + refetchRules(); + } + }} + /> + + {isRunningReview ? ( + + ) : error ? ( + + ) : data && data.findings.length > 0 ? ( + + ) : ( + + )} + + + + + Edit Security Rules + +
+ This allows you to add additional context about your project + specifically for security reviews. This content is saved to the{" "} + SECURITY_RULES.md file. This can + help catch additional issues or avoid flagging issues that are not + relevant for your app. +
+
+