Security review: fix multiple issues (#1699)
See #1692 <!-- CURSOR_SUMMARY --> > [!NOTE] > Adds multi-select with a "Fix X Issues" bulk action to Security Review (severity-sorted, with animated header button), clears selections on refresh, and improves streaming error logs; includes e2e coverage. > > - **Security Review UI (`src/components/preview_panel/SecurityPanel.tsx`)**: > - **Multi-select & Bulk Fix**: > - Add per-row checkboxes and a "Select all" checkbox in `FindingsTable`; sort by severity; ARIA labels. > - Track `selectedFindings`; clear on new data; header shows animated "Fix X Issues" button (`Wrench` icon) that creates one chat with a combined prompt for selected issues. > - **Fix Single Issue**: Preserve existing per-row "Fix Issue" flow with loading states. > - **Tests**: > - Add e2e test `security review - multi-select and fix issues` and snapshots for selection table and combined prompt. > - **IPC (`src/ipc/ipc_client.ts`)**: > - Enhance error logging (`console.error`) in `streamMessage` paths; simplify `cancelChatStream` (remove stale cleanup). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 08b9f92814e2a676d0a8de1badf7dc79cd82a14a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Add multi-select to the Security Review so you can select issues and fix them in one go. Improves error handling in chat streaming and adds an e2e test for the new flow. - New Features - Checkboxes per finding and a “Select all” checkbox, with severity-sorted rows. - Header shows an animated “Fix X Issues” button when items are selected; creates one chat with a combined prompt; clears selection after. - New e2e test: multi-select and bulk fix. - Bug Fixes - Clear selections when new review results load. - Better error logging in IpcClient for streaming failures; simplify cancelChatStream to avoid false errors. <sup>Written for commit 08b9f92814e2a676d0a8de1badf7dc79cd82a14a. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -43,3 +43,33 @@ test("security review - edit and use knowledge", async ({ po }) => {
|
|||||||
await po.waitForChatCompletion();
|
await po.waitForChatCompletion();
|
||||||
await po.snapshotServerDump("all-messages");
|
await po.snapshotServerDump("all-messages");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("security review - multi-select and fix issues", 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();
|
||||||
|
|
||||||
|
// Select the first two issues using individual checkboxes
|
||||||
|
const checkboxes = po.page.getByRole("checkbox");
|
||||||
|
// Skip the first checkbox (select all)
|
||||||
|
await checkboxes.nth(1).click();
|
||||||
|
await checkboxes.nth(2).click();
|
||||||
|
|
||||||
|
// Wait for the "Fix X Issues" button to appear
|
||||||
|
const fixSelectedButton = po.page.getByRole("button", {
|
||||||
|
name: "Fix 2 Issues",
|
||||||
|
});
|
||||||
|
await fixSelectedButton.waitFor({ state: "visible" });
|
||||||
|
|
||||||
|
// Click the fix selected button
|
||||||
|
await fixSelectedButton.click();
|
||||||
|
await po.waitForChatCompletion();
|
||||||
|
await po.snapshotMessages();
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
- paragraph: "Please fix the following 2 security issues in a simple and effective way:"
|
||||||
|
- list:
|
||||||
|
- listitem:
|
||||||
|
- strong: SQL Injection in User Lookup
|
||||||
|
- text: (critical severity)
|
||||||
|
- 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`"
|
||||||
|
- list:
|
||||||
|
- listitem:
|
||||||
|
- strong: Hardcoded AWS Credentials in Source Code
|
||||||
|
- text: (critical severity)
|
||||||
|
- 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: ": 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"
|
||||||
|
- paragraph:
|
||||||
|
- strong: Potential Solutions
|
||||||
|
- text: ":"
|
||||||
|
- list:
|
||||||
|
- listitem: Immediately rotate the exposed credentials in AWS IAM
|
||||||
|
- listitem:
|
||||||
|
- text: Use environment variables and add
|
||||||
|
- code: "`.env`"
|
||||||
|
- text: to
|
||||||
|
- code: "`.gitignore`"
|
||||||
|
- listitem: Implement AWS Secrets Manager or similar vault solution
|
||||||
|
- listitem:
|
||||||
|
- text: Scan git history and purge the credentials using tools like
|
||||||
|
- code: "`git-filter-repo`"
|
||||||
|
- paragraph:
|
||||||
|
- strong: Relevant Files
|
||||||
|
- text: ":"
|
||||||
|
- code: "`src/config/aws.ts`"
|
||||||
|
- text: ","
|
||||||
|
- code: "`src/services/s3-uploader.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
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
- table:
|
- table:
|
||||||
- rowgroup:
|
- rowgroup:
|
||||||
- row "Level Issue Action":
|
- row "Select all issues Level Issue Action":
|
||||||
|
- cell "Select all issues":
|
||||||
|
- checkbox "Select all issues"
|
||||||
- cell "Level"
|
- cell "Level"
|
||||||
- cell "Issue"
|
- cell "Issue"
|
||||||
- cell "Action"
|
- cell "Action"
|
||||||
- rowgroup:
|
- 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"':
|
- 'row "Select SQL Injection in User Lookup 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 "Select SQL Injection in User Lookup":
|
||||||
|
- checkbox "Select SQL Injection in User Lookup"
|
||||||
- cell "critical":
|
- cell "critical":
|
||||||
- img
|
- 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"':
|
- '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"':
|
||||||
@@ -20,7 +24,9 @@
|
|||||||
- img
|
- img
|
||||||
- cell "Fix Issue":
|
- cell "Fix Issue":
|
||||||
- button "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"':
|
- 'row "Select Hardcoded AWS Credentials in Source Code 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 "Select Hardcoded AWS Credentials in Source Code":
|
||||||
|
- checkbox "Select Hardcoded AWS Credentials in Source Code"
|
||||||
- cell "critical":
|
- cell "critical":
|
||||||
- img
|
- 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"':
|
- '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"':
|
||||||
@@ -35,7 +41,9 @@
|
|||||||
- img
|
- img
|
||||||
- cell "Fix Issue":
|
- cell "Fix Issue":
|
||||||
- button "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"':
|
- 'row "Select Missing Authentication on Admin Endpoints 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 "Select Missing Authentication on Admin Endpoints":
|
||||||
|
- checkbox "Select Missing Authentication on Admin Endpoints"
|
||||||
- cell "high":
|
- cell "high":
|
||||||
- img
|
- 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"':
|
- '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"':
|
||||||
@@ -50,7 +58,9 @@
|
|||||||
- img
|
- img
|
||||||
- cell "Fix Issue":
|
- cell "Fix Issue":
|
||||||
- button "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"':
|
- 'row "Select JWT Secret Using Default Value 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 "Select JWT Secret Using Default Value":
|
||||||
|
- checkbox "Select JWT Secret Using Default Value"
|
||||||
- cell "high":
|
- cell "high":
|
||||||
- img
|
- 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"':
|
- '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"':
|
||||||
@@ -65,7 +75,9 @@
|
|||||||
- img
|
- img
|
||||||
- cell "Fix Issue":
|
- cell "Fix Issue":
|
||||||
- button "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"':
|
- 'row "Select Unvalidated File Upload Extensions 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 "Select Unvalidated File Upload Extensions":
|
||||||
|
- checkbox "Select Unvalidated File Upload Extensions"
|
||||||
- cell "medium":
|
- cell "medium":
|
||||||
- img
|
- 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"':
|
- '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"':
|
||||||
@@ -80,7 +92,9 @@
|
|||||||
- img
|
- img
|
||||||
- cell "Fix Issue":
|
- cell "Fix Issue":
|
||||||
- button "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"':
|
- 'row "Select Missing CSRF Protection on State-Changing Operations 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 "Select Missing CSRF Protection on State-Changing Operations":
|
||||||
|
- checkbox "Select Missing CSRF Protection on State-Changing Operations"
|
||||||
- cell "medium":
|
- cell "medium":
|
||||||
- img
|
- 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"':
|
- '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"':
|
||||||
@@ -95,7 +109,9 @@
|
|||||||
- img
|
- img
|
||||||
- cell "Fix Issue":
|
- cell "Fix Issue":
|
||||||
- button "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"':
|
- 'row "Select Verbose Error Messages Expose Stack Traces 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 "Select Verbose Error Messages Expose Stack Traces":
|
||||||
|
- checkbox "Select Verbose Error Messages Expose Stack Traces"
|
||||||
- cell "low":
|
- cell "low":
|
||||||
- img
|
- 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"':
|
- '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"':
|
||||||
@@ -110,7 +126,9 @@
|
|||||||
- img
|
- img
|
||||||
- cell "Fix Issue":
|
- cell "Fix Issue":
|
||||||
- button "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"':
|
- 'row "Select Missing Security Headers 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 "Select Missing Security Headers":
|
||||||
|
- checkbox "Select Missing Security Headers"
|
||||||
- cell "low":
|
- cell "low":
|
||||||
- img
|
- 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"':
|
- '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"':
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ import {
|
|||||||
Info,
|
Info,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Pencil,
|
Pencil,
|
||||||
|
Wrench,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||||
import { showError } from "@/lib/toast";
|
import { showError } from "@/lib/toast";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import type { SecurityFinding, SecurityReviewResult } from "@/ipc/ipc_types";
|
import type { SecurityFinding, SecurityReviewResult } from "@/ipc/ipc_types";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { VanillaMarkdownParser } from "@/components/chat/DyadMarkdownParser";
|
import { VanillaMarkdownParser } from "@/components/chat/DyadMarkdownParser";
|
||||||
@@ -202,12 +204,36 @@ function SecurityHeader({
|
|||||||
onRun,
|
onRun,
|
||||||
data,
|
data,
|
||||||
onOpenEditRules,
|
onOpenEditRules,
|
||||||
|
selectedCount,
|
||||||
|
onFixSelected,
|
||||||
|
isFixingSelected,
|
||||||
}: {
|
}: {
|
||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
onRun: () => void;
|
onRun: () => void;
|
||||||
data?: SecurityReviewResult | undefined;
|
data?: SecurityReviewResult | undefined;
|
||||||
onOpenEditRules: () => void;
|
onOpenEditRules: () => void;
|
||||||
|
selectedCount: number;
|
||||||
|
onFixSelected: () => void;
|
||||||
|
isFixingSelected: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const [isButtonVisible, setIsButtonVisible] = useState(false);
|
||||||
|
const [shouldRender, setShouldRender] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedCount > 0) {
|
||||||
|
// Show immediately
|
||||||
|
setShouldRender(true);
|
||||||
|
// Trigger animation after render
|
||||||
|
setTimeout(() => setIsButtonVisible(true), 10);
|
||||||
|
} else {
|
||||||
|
// Trigger exit animation
|
||||||
|
setIsButtonVisible(false);
|
||||||
|
// Hide after animation completes
|
||||||
|
const timer = setTimeout(() => setShouldRender(false), 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [selectedCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky top-0 z-10 bg-background pt-3 pb-3 space-y-2">
|
<div className="sticky top-0 z-10 bg-background pt-3 pb-3 space-y-2">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
@@ -235,15 +261,62 @@ function SecurityHeader({
|
|||||||
</div>
|
</div>
|
||||||
{data && data.findings.length > 0 && <ReviewSummary data={data} />}
|
{data && data.findings.length > 0 && <ReviewSummary data={data} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-end gap-2">
|
||||||
<Button variant="outline" onClick={onOpenEditRules}>
|
<Button variant="outline" onClick={onOpenEditRules}>
|
||||||
<Pencil className="w-4 h-4" />
|
<Pencil className="w-4 h-4" />
|
||||||
Edit Security Rules
|
Edit Security Rules
|
||||||
</Button>
|
</Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onFixSelected}
|
||||||
|
className="gap-2 transition-all duration-300"
|
||||||
|
disabled={isFixingSelected}
|
||||||
|
style={{
|
||||||
|
visibility: shouldRender ? "visible" : "hidden",
|
||||||
|
opacity: isButtonVisible ? 1 : 0,
|
||||||
|
transform: isButtonVisible
|
||||||
|
? "translateY(0)"
|
||||||
|
: "translateY(-8px)",
|
||||||
|
pointerEvents: shouldRender ? "auto" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFixingSelected ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 animate-spin"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="m4 12a8 8 0 0 1 8-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 0 1 4 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Fixing {selectedCount} Issue{selectedCount !== 1 ? "s" : ""}
|
||||||
|
...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Wrench className="w-4 h-4" />
|
||||||
|
Fix {selectedCount} Issue{selectedCount !== 1 ? "s" : ""}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
<RunReviewButton isRunning={isRunning} onRun={onRun} />
|
<RunReviewButton isRunning={isRunning} onRun={onRun} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,12 +463,28 @@ function FindingsTable({
|
|||||||
onOpenDetails,
|
onOpenDetails,
|
||||||
onFix,
|
onFix,
|
||||||
fixingFindingKey,
|
fixingFindingKey,
|
||||||
|
selectedFindings,
|
||||||
|
onToggleSelection,
|
||||||
|
onToggleSelectAll,
|
||||||
}: {
|
}: {
|
||||||
findings: SecurityFinding[];
|
findings: SecurityFinding[];
|
||||||
onOpenDetails: (finding: SecurityFinding) => void;
|
onOpenDetails: (finding: SecurityFinding) => void;
|
||||||
onFix: (finding: SecurityFinding) => void;
|
onFix: (finding: SecurityFinding) => void;
|
||||||
fixingFindingKey?: string | null;
|
fixingFindingKey?: string | null;
|
||||||
|
selectedFindings: Set<string>;
|
||||||
|
onToggleSelection: (findingKey: string) => void;
|
||||||
|
onToggleSelectAll: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const sortedFindings = [...findings].sort(
|
||||||
|
(a, b) => getSeverityOrder(a.level) - getSeverityOrder(b.level),
|
||||||
|
);
|
||||||
|
|
||||||
|
const allSelected =
|
||||||
|
sortedFindings.length > 0 &&
|
||||||
|
sortedFindings.every((finding) =>
|
||||||
|
selectedFindings.has(createFindingKey(finding)),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="border rounded-lg overflow-hidden"
|
className="border rounded-lg overflow-hidden"
|
||||||
@@ -404,6 +493,13 @@ function FindingsTable({
|
|||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700">
|
<thead className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider w-12">
|
||||||
|
<Checkbox
|
||||||
|
checked={allSelected}
|
||||||
|
onCheckedChange={onToggleSelectAll}
|
||||||
|
aria-label="Select all issues"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider w-24">
|
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider w-24">
|
||||||
Level
|
Level
|
||||||
</th>
|
</th>
|
||||||
@@ -416,11 +512,7 @@ function FindingsTable({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{[...findings]
|
{sortedFindings.map((finding, index) => {
|
||||||
.sort(
|
|
||||||
(a, b) => getSeverityOrder(a.level) - getSeverityOrder(b.level),
|
|
||||||
)
|
|
||||||
.map((finding, index) => {
|
|
||||||
const isLongDescription =
|
const isLongDescription =
|
||||||
finding.description.length > DESCRIPTION_PREVIEW_LENGTH;
|
finding.description.length > DESCRIPTION_PREVIEW_LENGTH;
|
||||||
const displayDescription = isLongDescription
|
const displayDescription = isLongDescription
|
||||||
@@ -429,12 +521,21 @@ function FindingsTable({
|
|||||||
: finding.description;
|
: finding.description;
|
||||||
const findingKey = createFindingKey(finding);
|
const findingKey = createFindingKey(finding);
|
||||||
const isFixing = fixingFindingKey === findingKey;
|
const isFixing = fixingFindingKey === findingKey;
|
||||||
|
const isSelected = selectedFindings.has(findingKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors"
|
className="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors"
|
||||||
>
|
>
|
||||||
|
<td className="px-4 py-4 align-top">
|
||||||
|
<Checkbox
|
||||||
|
checked={isSelected}
|
||||||
|
onCheckedChange={() => onToggleSelection(findingKey)}
|
||||||
|
aria-label={`Select ${finding.title}`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td className="px-4 py-4 align-top">
|
<td className="px-4 py-4 align-top">
|
||||||
<SeverityBadge level={finding.level} />
|
<SeverityBadge level={finding.level} />
|
||||||
</td>
|
</td>
|
||||||
@@ -607,6 +708,10 @@ export const SecurityPanel = () => {
|
|||||||
const [rulesContent, setRulesContent] = useState("");
|
const [rulesContent, setRulesContent] = useState("");
|
||||||
const [fixingFindingKey, setFixingFindingKey] = useState<string | null>(null);
|
const [fixingFindingKey, setFixingFindingKey] = useState<string | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [selectedFindings, setSelectedFindings] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
const [isFixingSelected, setIsFixingSelected] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
content: fetchedRules,
|
content: fetchedRules,
|
||||||
@@ -623,6 +728,11 @@ export const SecurityPanel = () => {
|
|||||||
}
|
}
|
||||||
}, [fetchedRules]);
|
}, [fetchedRules]);
|
||||||
|
|
||||||
|
// Clear selections when data changes (e.g., after a new review)
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedFindings(new Set());
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
const handleSaveRules = async () => {
|
const handleSaveRules = async () => {
|
||||||
if (!selectedAppId) {
|
if (!selectedAppId) {
|
||||||
showError("No app selected");
|
showError("No app selected");
|
||||||
@@ -725,6 +835,82 @@ ${finding.description}`;
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleSelection = (findingKey: string) => {
|
||||||
|
setSelectedFindings((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(findingKey)) {
|
||||||
|
newSet.delete(findingKey);
|
||||||
|
} else {
|
||||||
|
newSet.add(findingKey);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleSelectAll = () => {
|
||||||
|
if (!data?.findings) return;
|
||||||
|
|
||||||
|
const sortedFindings = [...data.findings].sort(
|
||||||
|
(a, b) => getSeverityOrder(a.level) - getSeverityOrder(b.level),
|
||||||
|
);
|
||||||
|
|
||||||
|
const allKeys = sortedFindings.map((finding) => createFindingKey(finding));
|
||||||
|
const allSelected = allKeys.every((key) => selectedFindings.has(key));
|
||||||
|
|
||||||
|
if (allSelected) {
|
||||||
|
setSelectedFindings(new Set());
|
||||||
|
} else {
|
||||||
|
setSelectedFindings(new Set(allKeys));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFixSelected = async () => {
|
||||||
|
if (!selectedAppId || selectedFindings.size === 0 || !data?.findings) {
|
||||||
|
showError("No issues selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsFixingSelected(true);
|
||||||
|
|
||||||
|
// Get the selected findings
|
||||||
|
const findingsToFix = data.findings.filter((finding) =>
|
||||||
|
selectedFindings.has(createFindingKey(finding)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 } });
|
||||||
|
|
||||||
|
// Build a comprehensive prompt for all selected issues
|
||||||
|
const issuesList = findingsToFix
|
||||||
|
.map(
|
||||||
|
(finding, index) =>
|
||||||
|
`${index + 1}. **${finding.title}** (${finding.level} severity)\n${finding.description}`,
|
||||||
|
)
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const prompt = `Please fix the following ${findingsToFix.length} security issue${findingsToFix.length !== 1 ? "s" : ""} in a simple and effective way:
|
||||||
|
|
||||||
|
${issuesList}`;
|
||||||
|
|
||||||
|
await streamMessage({
|
||||||
|
prompt,
|
||||||
|
chatId,
|
||||||
|
onSettled: () => {
|
||||||
|
setIsFixingSelected(false);
|
||||||
|
setSelectedFindings(new Set());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
showError(`Failed to create fix chat: ${err}`);
|
||||||
|
setIsFixingSelected(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
}
|
}
|
||||||
@@ -746,6 +932,9 @@ ${finding.description}`;
|
|||||||
refetchRules();
|
refetchRules();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
selectedCount={selectedFindings.size}
|
||||||
|
onFixSelected={handleFixSelected}
|
||||||
|
isFixingSelected={isFixingSelected}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isRunningReview ? (
|
{isRunningReview ? (
|
||||||
@@ -761,6 +950,9 @@ ${finding.description}`;
|
|||||||
onOpenDetails={openFindingDetails}
|
onOpenDetails={openFindingDetails}
|
||||||
onFix={handleFixIssue}
|
onFix={handleFixIssue}
|
||||||
fixingFindingKey={fixingFindingKey}
|
fixingFindingKey={fixingFindingKey}
|
||||||
|
selectedFindings={selectedFindings}
|
||||||
|
onToggleSelection={handleToggleSelection}
|
||||||
|
onToggleSelectAll={handleToggleSelectAll}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<NoIssuesCard data={data} />
|
<NoIssuesCard data={data} />
|
||||||
|
|||||||
@@ -445,12 +445,14 @@ export class IpcClient {
|
|||||||
attachments: fileDataArray,
|
attachments: fileDataArray,
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
console.error("Error streaming message:", err);
|
||||||
showError(err);
|
showError(err);
|
||||||
onError(String(err));
|
onError(String(err));
|
||||||
this.chatStreams.delete(chatId);
|
this.chatStreams.delete(chatId);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
console.error("Error streaming message:", err);
|
||||||
showError(err);
|
showError(err);
|
||||||
onError(String(err));
|
onError(String(err));
|
||||||
this.chatStreams.delete(chatId);
|
this.chatStreams.delete(chatId);
|
||||||
@@ -465,6 +467,7 @@ export class IpcClient {
|
|||||||
selectedComponent,
|
selectedComponent,
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
console.error("Error streaming message:", err);
|
||||||
showError(err);
|
showError(err);
|
||||||
onError(String(err));
|
onError(String(err));
|
||||||
this.chatStreams.delete(chatId);
|
this.chatStreams.delete(chatId);
|
||||||
@@ -475,12 +478,6 @@ export class IpcClient {
|
|||||||
// Method to cancel an ongoing stream
|
// Method to cancel an ongoing stream
|
||||||
public cancelChatStream(chatId: number): void {
|
public cancelChatStream(chatId: number): void {
|
||||||
this.ipcRenderer.invoke("chat:cancel", chatId);
|
this.ipcRenderer.invoke("chat:cancel", chatId);
|
||||||
const callbacks = this.chatStreams.get(chatId);
|
|
||||||
if (callbacks) {
|
|
||||||
this.chatStreams.delete(chatId);
|
|
||||||
} else {
|
|
||||||
console.error("Tried canceling chat that doesn't exist");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new chat for an app
|
// Create a new chat for an app
|
||||||
|
|||||||
Reference in New Issue
Block a user