From 376d09102c20a96b4678df2cebc0e1777898b34e Mon Sep 17 00:00:00 2001 From: Hamed Mohamed Date: Wed, 21 Jan 2026 22:11:40 +0300 Subject: [PATCH 1/5] feat(backend): implement scan progress tracking, cancellation and database migration (scan.progress field) --- backend/prisma/schema.prisma | 1 + backend/src/controllers/scans.ts | 20 ++++++++++++++++++ backend/src/routes/scans.ts | 18 ++++++++++++++++ backend/src/services/scans.ts | 28 +++++++++++++++++++++++++ backend/src/worker/sqli/orchestrator.ts | 20 ++++++++++++++++++ 5 files changed, 87 insertions(+) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index ffb57e5..f0d5203 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -115,6 +115,7 @@ model Target { model Scan { id String @id @default(cuid()) status ScanStatus @default(PENDING) + progress Int @default(0) // 🆕 نسبة التقدم المئوية startedAt DateTime? completedAt DateTime? createdAt DateTime @default(now()) diff --git a/backend/src/controllers/scans.ts b/backend/src/controllers/scans.ts index c97de1f..8135ac9 100644 --- a/backend/src/controllers/scans.ts +++ b/backend/src/controllers/scans.ts @@ -63,3 +63,23 @@ export const listScans = async ( next(error); } }; + +export const cancelScan = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const userId = req.kullanici!.id; + const { id } = req.params; + + if (!id) { + return res.status(400).json({ message: 'Scan ID is required' }); + } + + await scanService.cancelScan(userId, id); + res.status(200).json({ message: 'Scan cancellation requested' }); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/routes/scans.ts b/backend/src/routes/scans.ts index 0ddc1ff..a4da369 100644 --- a/backend/src/routes/scans.ts +++ b/backend/src/routes/scans.ts @@ -133,4 +133,22 @@ router.get('/', kimlikDoğrula, listScansValidation, scanController.listScans); */ router.get('/:id', kimlikDoğrula, getScanValidation, scanController.getScan); +/** + * @swagger + * /scans/{id}/cancel: + * post: + * summary: Cancels an active scan + * tags: [Scans] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: { type: string } + * responses: + * 200: { description: Scan cancelled } + */ +router.post('/:id/cancel', kimlikDoğrula, scanController.cancelScan); + export default router; \ No newline at end of file diff --git a/backend/src/services/scans.ts b/backend/src/services/scans.ts index 840c72f..4af7085 100644 --- a/backend/src/services/scans.ts +++ b/backend/src/services/scans.ts @@ -174,4 +174,32 @@ export const listScansForOrg = async (userId: string, organizationId: string) => }); return scans; +}; + +/** + * يلغي فحصاً جارياً أو قيد الانتظار. + * @param userId - معرف المستخدم. + * @param scanId - معرف الفحص. + */ +export const cancelScan = async (userId: string, scanId: string) => { + // 1. التأكد من وجود الفحص وصلاحيات المستخدم + const scan = await getScanById(userId, scanId); + + // 2. تحديث الحالة فقط إذا كانت RUNNING أو QUEUED أو PENDING + const activeStatuses = ['RUNNING', 'QUEUED', 'PENDING']; + if (activeStatuses.includes(scan.status)) { + await prisma.scan.update({ + where: { id: scanId }, + data: { + status: ScanStatus.CANCELED, + completedAt: new Date() // نعتبره مكتملاً (متوقفاً) لتسجيل الوقت + } + }); + + // ملاحظة: إيقاف الـ Job الفعلي من BullMQ يتطلب معرف JobId. + // في هذه المرحلة، نكتفي بتحديث حالة قاعدة البيانات. + // الـ Worker يجب أن يتحقق من حالة الفحص في قاعدة البيانات قبل الاستمرار. + } + + return true; }; \ No newline at end of file diff --git a/backend/src/worker/sqli/orchestrator.ts b/backend/src/worker/sqli/orchestrator.ts index 453cac5..6a35dba 100644 --- a/backend/src/worker/sqli/orchestrator.ts +++ b/backend/src/worker/sqli/orchestrator.ts @@ -34,6 +34,11 @@ export async function runSqliScan(job: Job, prisma: PrismaClient): Promise if (await executeAuthBypassAttack(job, prisma)) totalVulnerabilities++; await job.updateProgress(10); // تحديث التقدم المبدئي + // 🆕 حفظ التقدم الأولي + try { + // @ts-ignore: Prisma Client types might be stale + await prisma.scan.update({ where: { id: scanId }, data: { progress: 10 } }); + } catch (e) { /* Ignore stale client error */ } // --- المرحلة الثانية: الفحص الدقيق لكل بارامتر (Waves 2-6) --- // نحتفظ بنسخة من البيانات الأصلية لتجنب تلوث البيانات أثناء التكرار @@ -73,6 +78,18 @@ export async function runSqliScan(job: Job, prisma: PrismaClient): Promise // المعادلة: نوزع 80% من التقدم على هذه المرحلة const progress = 10 + Math.floor(((i + 1) / totalParams) * 80); await job.updateProgress(progress); + + // 🆕 تحديث التقدم في قاعدة البيانات للعرض في الواجهة (مع حماية من الأخطاء) + try { + // @ts-ignore: Prisma Client types might be stale + await prisma.scan.update({ + where: { id: scanId }, + data: { progress: progress } + }); + } catch (e) { + // قد يفشل إذا لم يتم تحديث Prisma Client بعد، نتجاهل الخطأ لكي لا يتوقف الفحص + console.warn('[Orchestrator] Failed to sync progress to DB (Non-fatal)'); + } } // استعادة البيانات الأصلية للمرحلة الأخيرة @@ -84,12 +101,15 @@ export async function runSqliScan(job: Job, prisma: PrismaClient): Promise if (await executeSecondOrderAttack(job, prisma)) totalVulnerabilities++; await job.updateProgress(100); + // لا نحتاج لتحديث التقدم هنا لأن التحديث الأخير سيضع الحالة COMPLETED والتقدم 100 // --- إتمام المهمة --- + // @ts-ignore: Prisma Client types might be stale await prisma.scan.update({ where: { id: scanId }, data: { status: 'COMPLETED', + progress: 100, // 🆕 تأكيد الوصول لـ 100% completedAt: new Date(), // يمكن إضافة حقل لعدد النتائج إذا كان مدعوماً في قاعدة البيانات // findingsCount: totalVulnerabilities From f598d1a477993547fdbfda8dd800c902e2b05f76 Mon Sep 17 00:00:00 2001 From: Hamed Mohamed Date: Wed, 21 Jan 2026 22:12:29 +0300 Subject: [PATCH 2/5] feat(frontend): add scan progress UI, cancel button and visual improvements --- frontend/src/entities/scan/api/scanApi.ts | 5 +++ frontend/src/entities/scan/model/types.ts | 3 +- frontend/src/pages/scans/ui/Page.tsx | 50 ++++++++++++++++++----- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/frontend/src/entities/scan/api/scanApi.ts b/frontend/src/entities/scan/api/scanApi.ts index e4a41e3..71f89d2 100644 --- a/frontend/src/entities/scan/api/scanApi.ts +++ b/frontend/src/entities/scan/api/scanApi.ts @@ -17,5 +17,10 @@ export const scanApi = { getOne: async (id: string): Promise => { const { data } = await apiClient.get<{ data: Scan }>(`/scans/${id}`); return data.data; + }, + + // 🆕 إيقاف الفحص + cancel: async (id: string): Promise => { + await apiClient.post(`/scans/${id}/cancel`); } }; diff --git a/frontend/src/entities/scan/model/types.ts b/frontend/src/entities/scan/model/types.ts index b3aa92c..7333fdf 100644 --- a/frontend/src/entities/scan/model/types.ts +++ b/frontend/src/entities/scan/model/types.ts @@ -1,8 +1,9 @@ -export type ScanStatus = 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'STOPPED'; +export type ScanStatus = 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELED'; export interface Scan { id: string; status: ScanStatus; + progress?: number; // 🆕 نسبة التقدم (0-100) startedAt: string | null; completedAt: string | null; createdAt: string; diff --git a/frontend/src/pages/scans/ui/Page.tsx b/frontend/src/pages/scans/ui/Page.tsx index 08da0b3..f4b6cae 100644 --- a/frontend/src/pages/scans/ui/Page.tsx +++ b/frontend/src/pages/scans/ui/Page.tsx @@ -14,14 +14,23 @@ import { } from 'lucide-react'; import { scanApi } from '@/entities/scan/api/scanApi'; import { useAuthStore } from '@/features/auth/model/authStore'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import type { ScanStatus } from '@/entities/scan/model/types'; const ScansPage: React.FC = () => { + const queryClient = useQueryClient(); // Add this const { user } = useAuthStore(); const { t } = useTranslation(); + // Cancel Scan Mutation + const cancelMutation = useMutation({ + mutationFn: (scanId: string) => scanApi.cancel(scanId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['scans'] }); + }, + }); + // Fetch Scans with polling const { data: scans = [], isLoading, refetch, isRefetching } = useQuery({ queryKey: ['scans', user?.organizationId], @@ -36,6 +45,7 @@ const ScansPage: React.FC = () => { case 'PENDING': return 'text-blue-400'; case 'COMPLETED': return 'text-purple-400'; case 'FAILED': return 'text-cyber-red'; + case 'CANCELED': return 'text-white/40'; default: return 'text-white/40'; } }; @@ -46,6 +56,7 @@ const ScansPage: React.FC = () => { case 'PENDING': return ; case 'COMPLETED': return ; case 'FAILED': return ; + case 'CANCELED': return ; default: return ; } }; @@ -124,13 +135,27 @@ const ScansPage: React.FC = () => {
-
- +
+ {scan.status === 'RUNNING' && !scan.progress ? ( + // Indeterminate Loading State + + ) : ( + // Determined Progress or Static State + + )}
+

+ {scan.status === 'RUNNING' && !scan.progress ? 'ANALYZING...' : `${scan.progress || 0}%`} +

@@ -147,9 +172,14 @@ const ScansPage: React.FC = () => { - ) : scan.status === 'RUNNING' ? ( - ) : (

- {scan.status === 'RUNNING' && !scan.progress ? 'ANALYZING...' : `${scan.progress || 0}%`} + {scan.status === 'RUNNING' && scan.progress == null ? 'ANALYZING...' : `${scan.status === 'COMPLETED' ? 100 : (scan.progress ?? 0)}%`}

From 841f15f97c423f2865d2a42adb4745c368519953 Mon Sep 17 00:00:00 2001 From: Hamed Mohamed Date: Mon, 4 May 2026 11:50:20 +0300 Subject: [PATCH 5/5] Update frontend/src/pages/scans/ui/Page.tsx Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- frontend/src/pages/scans/ui/Page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/scans/ui/Page.tsx b/frontend/src/pages/scans/ui/Page.tsx index b390c88..cbbf669 100644 --- a/frontend/src/pages/scans/ui/Page.tsx +++ b/frontend/src/pages/scans/ui/Page.tsx @@ -136,7 +136,7 @@ const ScansPage: React.FC = () => {
- {scan.status === 'RUNNING' && !scan.progress ? ( + {scan.status === 'RUNNING' && scan.progress == null ? ( // Indeterminate Loading State