From c145125b1a5a94a344e02da5f534429734e273b8 Mon Sep 17 00:00:00 2001 From: gearnode Date: Wed, 2 Apr 2025 10:10:49 +0200 Subject: [PATCH] Add requested evidence Signed-off-by: gearnode --- .../mitigations/MitigationView.tsx | 445 ++++++- ...ationViewCreateEvidenceMutation.graphql.ts | 229 ++++ ...tionViewFulfillEvidenceMutation.graphql.ts | 226 ++++ .../MitigationViewQuery.graphql.ts | 4 +- ...ationViewUploadEvidenceMutation.graphql.ts | 4 +- pkg/coredata/evidence.go | 2 +- pkg/coredata/evidence_state.go | 25 +- pkg/coredata/migrations/20250402T083700Z.sql | 10 + pkg/coredata/migrations/20250402T093900Z.sql | 1 + pkg/mailer/mailer.go | 2 +- pkg/probo/evidence_service.go | 74 +- pkg/server/api/console/v1/schema.graphql | 39 +- pkg/server/api/console/v1/schema/schema.go | 1044 +++++++++++++---- pkg/server/api/console/v1/types/types.go | 47 +- pkg/server/api/console/v1/v1_resolver.go | 56 +- 15 files changed, 1856 insertions(+), 352 deletions(-) create mode 100644 apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewCreateEvidenceMutation.graphql.ts create mode 100644 apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewFulfillEvidenceMutation.graphql.ts create mode 100644 pkg/coredata/migrations/20250402T083700Z.sql create mode 100644 pkg/coredata/migrations/20250402T093900Z.sql diff --git a/apps/console/src/pages/organizations/mitigations/MitigationView.tsx b/apps/console/src/pages/organizations/mitigations/MitigationView.tsx index a43fe7aa8..bbb516813 100644 --- a/apps/console/src/pages/organizations/mitigations/MitigationView.tsx +++ b/apps/console/src/pages/organizations/mitigations/MitigationView.tsx @@ -17,30 +17,41 @@ import { useRelayEnvironment, } from "react-relay"; import { + AlertTriangle, + ArrowRight, + Check, CheckCircle2, - Plus, - Trash2, - FileIcon, - Loader2, ChevronDown, ChevronUp, + Clock, Download, Eye, File as FileGeneric, + FileIcon, FileText, Image, - X, - UserPlus, - UserMinus, - User, Link2, + Loader2, + LucideIcon, + MoreHorizontal, + Plus, Search, - Link as LinkIcon, - CheckSquare, ShieldCheck, - AlertTriangle, + Trash2, + Upload, + User, + UserMinus, + UserPlus, + X, } from "lucide-react"; -import { Card, CardContent } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { useToast } from "@/hooks/use-toast"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -83,7 +94,6 @@ import { MitigationViewSkeleton } from "./MitigationPage"; import { MitigationViewUpdateTaskStateMutation as MitigationViewUpdateTaskStateMutationType } from "./__generated__/MitigationViewUpdateTaskStateMutation.graphql"; import { MitigationViewCreateTaskMutation as MitigationViewCreateTaskMutationType } from "./__generated__/MitigationViewCreateTaskMutation.graphql"; import { MitigationViewDeleteTaskMutation as MitigationViewDeleteTaskMutationType } from "./__generated__/MitigationViewDeleteTaskMutation.graphql"; -import { MitigationViewUploadEvidenceMutation as MitigationViewUploadEvidenceMutationType } from "./__generated__/MitigationViewUploadEvidenceMutation.graphql"; import { MitigationViewDeleteEvidenceMutation as MitigationViewDeleteEvidenceMutationType } from "./__generated__/MitigationViewDeleteEvidenceMutation.graphql"; import { MitigationViewAssignTaskMutation as MitigationViewAssignTaskMutationType } from "./__generated__/MitigationViewAssignTaskMutation.graphql"; import { MitigationViewUnassignTaskMutation as MitigationViewUnassignTaskMutationType } from "./__generated__/MitigationViewUnassignTaskMutation.graphql"; @@ -105,6 +115,10 @@ import { MitigationViewRisksQuery, MitigationViewRisksQuery$data, } from "./__generated__/MitigationViewRisksQuery.graphql"; +import { MitigationViewCreateEvidenceMutation as MitigationViewCreateEvidenceMutationType } from "./__generated__/MitigationViewCreateEvidenceMutation.graphql"; +import { MitigationViewGetEvidenceFileUrlQuery as MitigationViewGetEvidenceFileUrlQueryType } from "./__generated__/MitigationViewGetEvidenceFileUrlQuery.graphql"; +import { MitigationViewFulfillEvidenceMutation as MitigationViewFulfillEvidenceMutationType } from "./__generated__/MitigationViewFulfillEvidenceMutation.graphql"; +import { MitigationViewUploadEvidenceMutation as MitigationViewUploadEvidenceMutationType } from "./__generated__/MitigationViewUploadEvidenceMutation.graphql"; // Function to format ISO8601 duration to human-readable format const formatDuration = (isoDuration: string): string => { @@ -238,12 +252,12 @@ const deleteTaskMutation = graphql` } `; -const uploadEvidenceMutation = graphql` - mutation MitigationViewUploadEvidenceMutation( - $input: UploadEvidenceInput! +const createEvidenceMutation = graphql` + mutation MitigationViewCreateEvidenceMutation( + $input: CreateEvidenceInput! $connections: [ID!]! ) { - uploadEvidence(input: $input) { + createEvidence(input: $input) { evidenceEdge @appendEdge(connections: $connections) { node { id @@ -416,6 +430,30 @@ const deleteControlMappingMutation = graphql` } `; +// Add a mutation to fulfill evidence +const fulfillEvidenceMutation = graphql` + mutation MitigationViewFulfillEvidenceMutation( + $input: FulfillEvidenceInput! + $connections: [ID!]! + ) { + fulfillEvidence(input: $input) { + evidenceEdge @appendEdge(connections: $connections) { + node { + id + filename + fileUrl + mimeType + type + url + size + state + createdAt + } + } + } + } +`; + // Add query to fetch risks linked to a mitigation const mitigationRisksQuery = graphql` query MitigationViewRisksQuery($mitigationId: ID!) { @@ -641,9 +679,9 @@ function MitigationViewContent({ useMutation(createTaskMutation); const [deleteTask] = useMutation(deleteTaskMutation); - const [uploadEvidence] = - useMutation( - uploadEvidenceMutation + const [createEvidence] = + useMutation( + createEvidenceMutation ); const [deleteEvidence] = useMutation( @@ -653,6 +691,10 @@ function MitigationViewContent({ useMutation(assignTaskMutation); const [unassignTask] = useMutation(unassignTaskMutation); + const [fulfillEvidence] = + useMutation( + fulfillEvidenceMutation + ); const [updateMitigationState] = useMutation( @@ -723,6 +765,14 @@ function MitigationViewContent({ const [linkEvidenceDescription, setLinkEvidenceDescription] = useState(""); const [activeTab, setActiveTab] = useState<"file" | "link">("file"); + // State for fulfill evidence dialog + const [fulfillEvidenceDialogOpen, setFulfillEvidenceDialogOpen] = + useState(false); + const [evidenceToFulfill, setEvidenceToFulfill] = useState<{ + id: string; + filename: string; + } | null>(null); + // Add state for selected task panel const [selectedTask, setSelectedTask] = useState<(typeof tasks)[0] | null>( null @@ -955,7 +1005,7 @@ function MitigationViewContent({ ); }; - const handleUploadEvidence = (taskId: string, taskName: string) => { + const handleCreateEvidence = (taskId: string, taskName: string) => { setTaskForEvidence({ id: taskId, name: taskName }); setEvidenceDialogOpen(true); // Reset form fields @@ -980,7 +1030,7 @@ function MitigationViewContent({ // Get the evidence connection ID for this task const evidenceConnectionId = getEvidenceConnectionId(taskForEvidence.id); - uploadEvidence({ + createEvidence({ variables: { input: { taskId: taskForEvidence.id, @@ -1043,7 +1093,7 @@ function MitigationViewContent({ const description = linkEvidenceDescription.trim() || `Link to ${linkEvidenceUrl}`; - uploadEvidence({ + createEvidence({ variables: { input: { taskId: taskForEvidence.id, @@ -1109,7 +1159,7 @@ function MitigationViewContent({ // Get the evidence connection ID for this task const evidenceConnectionId = getEvidenceConnectionId(taskId); - uploadEvidence({ + createEvidence({ variables: { input: { taskId: taskId, @@ -1236,6 +1286,126 @@ function MitigationViewContent({ setIsDeleteEvidenceOpen(true); }; + const handleFulfillEvidence = (evidenceId: string, filename: string) => { + setEvidenceToFulfill({ id: evidenceId, filename }); + setFulfillEvidenceDialogOpen(true); + setActiveTab("file"); + setLinkEvidenceUrl(""); + setLinkEvidenceDescription(""); + }; + + const handleFulfillEvidenceWithFile = ( + e: React.ChangeEvent + ) => { + if (!e.target.files || e.target.files.length === 0 || !evidenceToFulfill) + return; + + const file = e.target.files[0]; + + // Show toast for upload started + toast({ + title: "Uploading evidence", + description: `Uploading ${file.name}...`, + variant: "default", + }); + + const evidenceId = evidenceToFulfill.id; + // Get connection ID for the parent task (assuming it's available in the view) + const task = tasks.find((task) => + task.evidences?.edges.some((edge) => edge?.node?.id === evidenceId) + ); + + const evidenceConnectionId = task?.id + ? getEvidenceConnectionId(task.id) + : null; + + fulfillEvidence({ + variables: { + input: { + evidenceId: evidenceId, + file: null, + }, + connections: evidenceConnectionId ? [evidenceConnectionId] : [], + }, + uploadables: { + "input.file": file, + }, + onCompleted: () => { + setFulfillEvidenceDialogOpen(false); + setEvidenceToFulfill(null); + toast({ + title: "Evidence uploaded", + description: "Evidence has been fulfilled successfully.", + variant: "default", + }); + // Reset the file input + if (hiddenFileInputRef.current) { + hiddenFileInputRef.current.value = ""; + } + }, + onError: (error) => { + toast({ + title: "Error uploading evidence", + description: error.message, + variant: "destructive", + }); + }, + }); + }; + + const handleFulfillEvidenceWithLink = () => { + if (!evidenceToFulfill) return; + + // Validate URL + if (!linkEvidenceUrl.trim()) { + toast({ + title: "Error", + description: "Please provide a URL for the evidence", + variant: "destructive", + }); + return; + } + + // Get connection ID for the parent task + const task = tasks.find((task) => + task.evidences?.edges.some( + (edge) => edge?.node?.id === evidenceToFulfill.id + ) + ); + + const evidenceConnectionId = task?.id + ? getEvidenceConnectionId(task.id) + : null; + + fulfillEvidence({ + variables: { + input: { + evidenceId: evidenceToFulfill.id, + url: linkEvidenceUrl, + }, + connections: evidenceConnectionId ? [evidenceConnectionId] : [], + }, + onCompleted: () => { + setFulfillEvidenceDialogOpen(false); + setEvidenceToFulfill(null); + setLinkEvidenceUrl(""); + setLinkEvidenceDescription(""); + toast({ + title: "Evidence updated", + description: "Evidence has been fulfilled with link successfully.", + variant: "default", + }); + }, + onError: (error) => { + toast({ + title: "Error updating evidence", + description: error.message, + variant: "destructive", + }); + }, + }); + }; + const confirmDeleteEvidence = () => { if (!evidenceToDelete) return; @@ -1789,7 +1959,7 @@ function MitigationViewContent({ > - + Tasks {tasks.length > 0 && ( @@ -1830,7 +2000,7 @@ function MitigationViewContent({ className="flex items-center gap-1" onClick={handleOpenControlMappingDialog} > - + Map to Controls @@ -1967,7 +2137,7 @@ function MitigationViewContent({ {isLinkingControl ? ( ) : ( - + )} Link @@ -2442,7 +2612,7 @@ function MitigationViewContent({ onClick={(e) => { e.stopPropagation(); if (task?.id && task?.name) { - handleUploadEvidence(task.id, task.name); + handleCreateEvidence(task.id, task.name); } }} title="Add Evidence" @@ -2512,16 +2682,31 @@ function MitigationViewContent({ )}
-
+
{evidence.filename} + {evidence.state === "REQUESTED" && ( + + Requested + + )}
- {evidence.type === "FILE" ? ( + {evidence.state === "REQUESTED" ? ( + + Waiting for fulfillment + + ) : evidence.type === "FILE" ? ( <> {formatFileSize(evidence.size)} • + + {formatDate(evidence.createdAt)} + ) : evidence.url ? ( <> @@ -2529,16 +2714,34 @@ function MitigationViewContent({ {evidence.url} • + + {formatDate(evidence.createdAt)} + - ) : null} - - {formatDate(evidence.createdAt)} - + ) : ( + + {formatDate(evidence.createdAt)} + + )}
- {evidence.type === "FILE" ? ( + {evidence.state === "REQUESTED" ? ( + + ) : evidence.type === "FILE" ? ( <> {evidence.mimeType.startsWith( "image/" @@ -2583,13 +2786,11 @@ function MitigationViewContent({
-
+
{evidence.filename} + {evidence.state === "REQUESTED" && ( + + Requested + + )}
- {evidence.type === "FILE" ? ( + {evidence.state === "REQUESTED" ? ( + + Waiting for fulfillment + + ) : evidence.type === "FILE" ? ( <> {formatFileSize(evidence.size)} • + + {formatDate(evidence.createdAt)} + ) : evidence.url ? ( <> @@ -2991,20 +3207,41 @@ function MitigationViewContent({ {evidence.url} • + + {formatDate(evidence.createdAt)} + - ) : null} - {formatDate(evidence.createdAt)} + ) : ( + + {formatDate(evidence.createdAt)} + + )}
- {evidence.type === "FILE" ? ( + {evidence.state === "REQUESTED" ? ( + + ) : evidence.type === "FILE" ? ( <> {evidence.mimeType.startsWith("image/") ? ( +
+ + + +
+
+ + setLinkEvidenceUrl(e.target.value)} + placeholder="https://example.com" + type="url" + /> +
+ +
+ +