diff --git a/app/admin/scope/page.tsx b/app/admin/scope/page.tsx index ed5a1bc..5973c23 100644 --- a/app/admin/scope/page.tsx +++ b/app/admin/scope/page.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import Link from "next/link"; import SiteHeader from "../../components/SiteHeader"; import SiteFooter from "../../components/SiteFooter"; @@ -13,11 +13,28 @@ interface Scope { description: string | null; targetType: string; status: string; + allowedVulnTypes: string | null; // JSON array string + severityRestriction: string | null; // JSON array string + notes: string | null; + exclusionPaths: string | null; + deletedAt: number | null; createdBy: string; createdAt: number; updatedAt: number; } +const ALL_VULN_TYPES = [ + 'Broken Access Control', 'Cryptographic Failure', 'Injection (SQL / XSS / Command / SSTI)', + 'Insecure Design', 'Security Misconfiguration', 'Vulnerable or Outdated Component', + 'Authentication / Session Failure', 'Software & Data Integrity Failure', + 'SSRF (Server-Side Request Forgery)', 'Business Logic Flaw', + 'Information Disclosure / Data Leak', 'IDOR (Insecure Direct Object Reference)', + 'Open Redirect', 'Clickjacking / UI Redressing', 'CORS Misconfiguration', + 'Path Traversal / File Inclusion', 'Other', +] as const; + +const ALL_SEVERITIES = ['Critical', 'High', 'Medium', 'Low', 'Info'] as const; + const TARGET_TYPE_LABELS: Record = { web_app: "Web Application", api: "API", @@ -29,6 +46,7 @@ const STATUS_COLORS: Record = { active: "bg-green-100 text-green-700 border-green-200", deprecated: "bg-yellow-100 text-yellow-700 border-yellow-200", out_of_scope: "bg-red-100 text-red-700 border-red-200", + archived: "bg-gray-100 text-gray-700 border-gray-200", }; export default function ScopeManagement() { @@ -42,18 +60,42 @@ export default function ScopeManagement() { description: "", targetType: "web_app", status: "active", + allowedVulnTypes: [] as string[], + severityRestriction: [] as string[], + notes: "", + exclusionPaths: "", }); const [toast, setToast] = useState<{ message: string; type: "success" | "error" | "info" } | null>(null); const [confirmDialog, setConfirmDialog] = useState<{ id: string; domain: string } | null>(null); + const [showArchived, setShowArchived] = useState(false); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 25; const [searchQuery, setSearchQuery] = useState(""); const [sortField, setSortField] = useState(null); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); + const fetchScopes = useCallback(async () => { + setLoading(true); + try { + const params = showArchived ? '?include_archived=true' : ''; + const res = await fetch(`/api/admin/scopes${params}`); + if (!res.ok) throw new Error('Failed to fetch scopes'); + const data = await res.json(); + setScopes(data.scopes); + } catch (err) { + console.error('[fetchScopes]', err); + setToast({ + message: 'Failed to load scopes', + type: 'error' + }); + } finally { + setLoading(false); + } + }, [showArchived]); + useEffect(() => { fetchScopes(); - }, []); + }, [fetchScopes]); // Search and filter logic const filteredScopes = scopes.filter(scope => { @@ -63,7 +105,8 @@ export default function ScopeManagement() { scope.domain.toLowerCase().includes(query) || (scope.description && scope.description.toLowerCase().includes(query)) || scope.targetType.toLowerCase().includes(query) || - scope.status.toLowerCase().includes(query) + scope.status.toLowerCase().includes(query) || + (scope.deletedAt !== null && 'archived'.includes(query)) ); }); @@ -71,8 +114,8 @@ export default function ScopeManagement() { const sortedScopes = [...filteredScopes].sort((a, b) => { if (!sortField) return 0; - let aVal: any = a[sortField]; - let bVal: any = b[sortField]; + let aVal: string | number | null = a[sortField]; + let bVal: string | number | null = b[sortField]; // Handle null values if (aVal === null || aVal === undefined) return 1; @@ -118,24 +161,6 @@ export default function ScopeManagement() { return {sortDirection === 'asc' ? '↑' : '↓'}; }; - async function fetchScopes() { - setLoading(true); - try { - const res = await fetch('/api/admin/scopes'); - if (!res.ok) throw new Error('Failed to fetch scopes'); - const data = await res.json(); - setScopes(data.scopes); - } catch (err) { - console.error('[fetchScopes]', err); - setToast({ - message: 'Failed to load scopes', - type: 'error' - }); - } finally { - setLoading(false); - } - } - async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setSubmitting(true); @@ -158,7 +183,7 @@ export default function ScopeManagement() { await fetchScopes(); setShowAddModal(false); setEditingScope(null); - setFormData({ domain: "", description: "", targetType: "web_app", status: "active" }); + setFormData({ domain: "", description: "", targetType: "web_app", status: "active", allowedVulnTypes: [], severityRestriction: [], notes: "", exclusionPaths: "" }); setToast({ message: editingScope ? 'Scope updated successfully!' : 'Scope added successfully!', type: 'success' @@ -195,7 +220,7 @@ export default function ScopeManagement() { await fetchScopes(); setToast({ - message: 'Scope deleted successfully!', + message: 'Scope archived successfully!', type: 'success' }); } catch (err: unknown) { @@ -207,6 +232,33 @@ export default function ScopeManagement() { } } + async function restoreScope(id: string) { + try { + const res = await fetch(`/api/admin/scopes/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ restore: true }), + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || 'Failed to restore scope'); + } + + await fetchScopes(); + setToast({ + message: 'Scope restored successfully!', + type: 'success' + }); + } catch (err: unknown) { + console.error('[restoreScope]', err); + setToast({ + message: err instanceof Error ? err.message : 'Failed to restore scope', + type: 'error' + }); + } + } + function openEditModal(scope: Scope) { setEditingScope(scope); setFormData({ @@ -214,6 +266,10 @@ export default function ScopeManagement() { description: scope.description || "", targetType: scope.targetType, status: scope.status, + allowedVulnTypes: scope.allowedVulnTypes ? JSON.parse(scope.allowedVulnTypes) : [], + severityRestriction: scope.severityRestriction ? JSON.parse(scope.severityRestriction) : [], + notes: scope.notes || "", + exclusionPaths: scope.exclusionPaths || "", }); setShowAddModal(true); } @@ -221,7 +277,19 @@ export default function ScopeManagement() { function closeModal() { setShowAddModal(false); setEditingScope(null); - setFormData({ domain: "", description: "", targetType: "web_app", status: "active" }); + setFormData({ domain: "", description: "", targetType: "web_app", status: "active", allowedVulnTypes: [], severityRestriction: [], notes: "", exclusionPaths: "" }); + } + + function toggleArrayField(field: 'allowedVulnTypes' | 'severityRestriction', value: string) { + setFormData(prev => { + const current = prev[field]; + return { + ...prev, + [field]: current.includes(value) + ? current.filter(v => v !== value) + : [...current, value], + }; + }); } return ( @@ -242,6 +310,16 @@ export default function ScopeManagement() { > + Add Target +

Active

-

{scopes.filter(s => s.status === 'active').length}

+

{scopes.filter(s => s.status === 'active' && s.deletedAt === null).length}

Deprecated

-

{scopes.filter(s => s.status === 'deprecated').length}

+

{scopes.filter(s => s.status === 'deprecated' && s.deletedAt === null).length}

-

Out of Scope

-

{scopes.filter(s => s.status === 'out_of_scope').length}

+

{showArchived ? 'Archived' : 'Out of Scope'}

+

+ {showArchived + ? scopes.filter(s => s.deletedAt !== null).length + : scopes.filter(s => s.status === 'out_of_scope' && s.deletedAt === null).length} +

@@ -337,7 +419,7 @@ export default function ScopeManagement() { {paginatedScopes.map((scope) => ( - +

{scope.domain}

@@ -350,8 +432,8 @@ export default function ScopeManagement() { - - {scope.status.replace('_', ' ')} + + {scope.deletedAt !== null ? 'archived' : scope.status.replace('_', ' ')} @@ -359,18 +441,29 @@ export default function ScopeManagement() {
- - + {scope.deletedAt === null ? ( + <> + + + + ) : ( + + )}
@@ -402,7 +495,7 @@ export default function ScopeManagement() { {/* Add/Edit Modal */} {showAddModal && (
-
+

{editingScope ? 'Edit Target' : 'Add New Target'}

@@ -465,6 +558,82 @@ export default function ScopeManagement() {
+ {/* Allowed Vulnerability Types */} +
+ +
+ {ALL_VULN_TYPES.map(type => ( + + ))} +
+
+ + {/* Severity Restriction */} +
+ +
+ {ALL_SEVERITIES.map(sev => ( + + ))} +
+
+ + {/* Notes */} +
+ +