Add requested evidence

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-04-02 10:10:49 +02:00
parent 2f9f04c984
commit c145125b1a
15 changed files with 1856 additions and 352 deletions

View File

@@ -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<MitigationViewCreateTaskMutationType>(createTaskMutation);
const [deleteTask] =
useMutation<MitigationViewDeleteTaskMutationType>(deleteTaskMutation);
const [uploadEvidence] =
useMutation<MitigationViewUploadEvidenceMutationType>(
uploadEvidenceMutation
const [createEvidence] =
useMutation<MitigationViewCreateEvidenceMutationType>(
createEvidenceMutation
);
const [deleteEvidence] =
useMutation<MitigationViewDeleteEvidenceMutationType>(
@@ -653,6 +691,10 @@ function MitigationViewContent({
useMutation<MitigationViewAssignTaskMutationType>(assignTaskMutation);
const [unassignTask] =
useMutation<MitigationViewUnassignTaskMutationType>(unassignTaskMutation);
const [fulfillEvidence] =
useMutation<MitigationViewFulfillEvidenceMutationType>(
fulfillEvidenceMutation
);
const [updateMitigationState] =
useMutation<MitigationViewUpdateMitigationStateMutationType>(
@@ -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<HTMLInputElement>
) => {
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({
>
<TabsList className="mb-4">
<TabsTrigger value="tasks" className="flex items-center gap-2">
<CheckSquare className="w-4 h-4" />
<Check className="w-4 h-4" />
Tasks
{tasks.length > 0 && (
<span className="ml-1.5 bg-blue-100 text-blue-800 rounded-full text-xs px-2 py-0.5">
@@ -1830,7 +2000,7 @@ function MitigationViewContent({
className="flex items-center gap-1"
onClick={handleOpenControlMappingDialog}
>
<LinkIcon className="w-4 h-4" />
<Link2 className="w-4 h-4" />
<span>Map to Controls</span>
</Button>
</div>
@@ -1967,7 +2137,7 @@ function MitigationViewContent({
{isLinkingControl ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<LinkIcon className="w-4 h-4" />
<Link2 className="w-4 h-4" />
)}
<span className="ml-1">Link</span>
</Button>
@@ -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({
)}
</div>
<div>
<div className="text-sm font-medium text-gray-800">
<div className="text-sm font-medium text-gray-800 flex items-center gap-2">
{evidence.filename}
{evidence.state === "REQUESTED" && (
<Badge
variant="outline"
className="text-xs bg-yellow-50 text-yellow-700 border-yellow-200"
>
Requested
</Badge>
)}
</div>
<div className="text-xs text-gray-500 flex items-center gap-2 mt-0.5">
{evidence.type === "FILE" ? (
{evidence.state === "REQUESTED" ? (
<span className="italic text-yellow-600">
Waiting for fulfillment
</span>
) : evidence.type === "FILE" ? (
<>
<span className="font-medium text-gray-600">
{formatFileSize(evidence.size)}
</span>
<span>•</span>
<span>
{formatDate(evidence.createdAt)}
</span>
</>
) : evidence.url ? (
<>
@@ -2529,16 +2714,34 @@ function MitigationViewContent({
{evidence.url}
</span>
<span>•</span>
<span>
{formatDate(evidence.createdAt)}
</span>
</>
) : null}
<span>
{formatDate(evidence.createdAt)}
</span>
) : (
<span>
{formatDate(evidence.createdAt)}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-1">
{evidence.type === "FILE" ? (
{evidence.state === "REQUESTED" ? (
<button
onClick={(e) => {
e.stopPropagation();
handleFulfillEvidence(
evidence.id,
evidence.filename
);
}}
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
title="Fulfill Evidence"
>
<Upload className="w-4 h-4 text-green-600" />
</button>
) : evidence.type === "FILE" ? (
<>
{evidence.mimeType.startsWith(
"image/"
@@ -2583,13 +2786,11 @@ function MitigationViewContent({
<button
onClick={(e) => {
e.stopPropagation();
if (task.id) {
handleDeleteEvidence(
evidence.id,
evidence.filename,
task.id
);
}
handleDeleteEvidence(
evidence.id,
evidence.filename,
task.id
);
}}
className="p-1.5 rounded-full hover:bg-red-50 hover:shadow-sm transition-all"
title="Delete"
@@ -2947,7 +3148,7 @@ function MitigationViewContent({
variant="outline"
size="sm"
onClick={() =>
handleUploadEvidence(selectedTask.id, selectedTask.name)
handleCreateEvidence(selectedTask.id, selectedTask.name)
}
className="flex items-center gap-1 text-xs"
>
@@ -2974,16 +3175,31 @@ function MitigationViewContent({
{getFileIcon(evidence.mimeType, evidence.type)}
</div>
<div>
<div className="text-sm font-medium text-gray-800">
<div className="text-sm font-medium text-gray-800 flex items-center gap-2">
{evidence.filename}
{evidence.state === "REQUESTED" && (
<Badge
variant="outline"
className="text-xs bg-yellow-50 text-yellow-700 border-yellow-200"
>
Requested
</Badge>
)}
</div>
<div className="text-xs text-gray-500 flex items-center gap-2 mt-0.5">
{evidence.type === "FILE" ? (
{evidence.state === "REQUESTED" ? (
<span className="italic text-yellow-600">
Waiting for fulfillment
</span>
) : evidence.type === "FILE" ? (
<>
<span className="font-medium text-gray-600">
{formatFileSize(evidence.size)}
</span>
<span>•</span>
<span>
{formatDate(evidence.createdAt)}
</span>
</>
) : evidence.url ? (
<>
@@ -2991,20 +3207,41 @@ function MitigationViewContent({
{evidence.url}
</span>
<span>•</span>
<span>
{formatDate(evidence.createdAt)}
</span>
</>
) : null}
<span>{formatDate(evidence.createdAt)}</span>
) : (
<span>
{formatDate(evidence.createdAt)}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-1">
{evidence.type === "FILE" ? (
{evidence.state === "REQUESTED" ? (
<button
onClick={(e) => {
e.stopPropagation();
handleFulfillEvidence(
evidence.id,
evidence.filename
);
}}
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
title="Fulfill Evidence"
>
<Upload className="w-4 h-4 text-green-600" />
</button>
) : evidence.type === "FILE" ? (
<>
{evidence.mimeType.startsWith("image/") ? (
<button
onClick={() =>
handlePreviewEvidence(evidence)
}
onClick={(e) => {
e.stopPropagation();
handlePreviewEvidence(evidence);
}}
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
title="Preview Image"
>
@@ -3013,7 +3250,7 @@ function MitigationViewContent({
) : (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handlePreviewEvidence(evidence);
}}
className="p-1.5 rounded-full hover:bg-white hover:shadow-sm transition-all"
@@ -3026,7 +3263,7 @@ function MitigationViewContent({
) : evidence.url ? (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (evidence.url) {
window.open(evidence.url, "_blank");
}
@@ -3039,11 +3276,11 @@ function MitigationViewContent({
) : null}
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleDeleteEvidence(
evidence.id,
evidence.filename,
selectedTask.id
task.id
);
}}
className="p-1.5 rounded-full hover:bg-red-50 hover:shadow-sm transition-all"
@@ -3353,6 +3590,104 @@ function MitigationViewContent({
</DialogFooter>
</DialogContent>
</Dialog>
{/* Fulfill Evidence Dialog */}
<Dialog
open={fulfillEvidenceDialogOpen}
onOpenChange={setFulfillEvidenceDialogOpen}
>
<DialogContent className="sm:max-w-[525px]">
<DialogHeader>
<DialogTitle>Fulfill Evidence</DialogTitle>
<DialogDescription>
Provide the requested evidence: {evidenceToFulfill?.filename}
</DialogDescription>
</DialogHeader>
<Tabs
value={activeTab}
onValueChange={(value: string) =>
setActiveTab(value as "file" | "link")
}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="file">Document</TabsTrigger>
<TabsTrigger value="link">Link</TabsTrigger>
</TabsList>
<TabsContent value="file" className="space-y-4">
<div className="space-y-4 pt-4">
<p>Upload a document to fulfill this evidence request.</p>
<Button
onClick={() => {
if (hiddenFileInputRef.current) {
// Set a temp onchange handler for the file input
const originalOnChange =
hiddenFileInputRef.current.onchange;
hiddenFileInputRef.current.onchange = (e) => {
hiddenFileInputRef.current!.onchange =
originalOnChange as any;
handleFulfillEvidenceWithFile({
target: {
files: (e.target as HTMLInputElement)?.files,
},
} as React.ChangeEvent<HTMLInputElement>);
};
hiddenFileInputRef.current.click();
setFulfillEvidenceDialogOpen(false);
}
}}
className="w-full"
>
Select Document
</Button>
</div>
</TabsContent>
<TabsContent value="link" className="space-y-4 pt-4">
<div className="space-y-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="evidence-url">URL</Label>
<Input
id="evidence-url"
value={linkEvidenceUrl}
onChange={(e) => setLinkEvidenceUrl(e.target.value)}
placeholder="https://example.com"
type="url"
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="evidence-description">
Description (optional)
</Label>
<Textarea
id="evidence-description"
value={linkEvidenceDescription}
onChange={(e) => setLinkEvidenceDescription(e.target.value)}
placeholder="Describe this evidence (optional)"
className="min-h-[100px]"
/>
</div>
</div>
</TabsContent>
</Tabs>
<DialogFooter>
<Button
variant="outline"
onClick={() => setFulfillEvidenceDialogOpen(false)}
>
Cancel
</Button>
{activeTab === "link" && (
<Button onClick={handleFulfillEvidenceWithLink}>
Submit Link
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
</PageTemplate>
);
}

View File

@@ -0,0 +1,229 @@
/**
* @generated SignedSource<<beb765793d4559382ee625dffd7a510c>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "FULFILLED" | "REQUESTED";
export type EvidenceType = "FILE" | "LINK";
export type CreateEvidenceInput = {
description: string;
file?: any | null | undefined;
name: string;
taskId: string;
type: EvidenceType;
url?: string | null | undefined;
};
export type MitigationViewCreateEvidenceMutation$variables = {
connections: ReadonlyArray<string>;
input: CreateEvidenceInput;
};
export type MitigationViewCreateEvidenceMutation$data = {
readonly createEvidence: {
readonly evidenceEdge: {
readonly node: {
readonly createdAt: string;
readonly fileUrl: string | null | undefined;
readonly filename: string;
readonly id: string;
readonly mimeType: string;
readonly size: number;
readonly state: EvidenceState;
readonly type: EvidenceType;
readonly url: string | null | undefined;
};
};
};
};
export type MitigationViewCreateEvidenceMutation = {
response: MitigationViewCreateEvidenceMutation$data;
variables: MitigationViewCreateEvidenceMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "EvidenceEdge",
"kind": "LinkedField",
"name": "evidenceEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Evidence",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "filename",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fileUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "type",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "url",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "MitigationViewCreateEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateEvidencePayload",
"kind": "LinkedField",
"name": "createEvidence",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "MitigationViewCreateEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateEvidencePayload",
"kind": "LinkedField",
"name": "createEvidence",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "appendEdge",
"key": "",
"kind": "LinkedHandle",
"name": "evidenceEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "ca3148cb5b3c59fec3bd9ff9c281f364",
"id": null,
"metadata": {},
"name": "MitigationViewCreateEvidenceMutation",
"operationKind": "mutation",
"text": "mutation MitigationViewCreateEvidenceMutation(\n $input: CreateEvidenceInput!\n) {\n createEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n type\n url\n size\n state\n createdAt\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "dac05ab414b2e09e801df5b0be9532a3";
export default node;

View File

@@ -0,0 +1,226 @@
/**
* @generated SignedSource<<c12714c0dab6d31865a2b49936566f05>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "FULFILLED" | "REQUESTED";
export type EvidenceType = "FILE" | "LINK";
export type FulfillEvidenceInput = {
evidenceId: string;
file?: any | null | undefined;
url?: string | null | undefined;
};
export type MitigationViewFulfillEvidenceMutation$variables = {
connections: ReadonlyArray<string>;
input: FulfillEvidenceInput;
};
export type MitigationViewFulfillEvidenceMutation$data = {
readonly fulfillEvidence: {
readonly evidenceEdge: {
readonly node: {
readonly createdAt: string;
readonly fileUrl: string | null | undefined;
readonly filename: string;
readonly id: string;
readonly mimeType: string;
readonly size: number;
readonly state: EvidenceState;
readonly type: EvidenceType;
readonly url: string | null | undefined;
};
};
};
};
export type MitigationViewFulfillEvidenceMutation = {
response: MitigationViewFulfillEvidenceMutation$data;
variables: MitigationViewFulfillEvidenceMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "EvidenceEdge",
"kind": "LinkedField",
"name": "evidenceEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Evidence",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "filename",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fileUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "type",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "url",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "MitigationViewFulfillEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "FulfillEvidencePayload",
"kind": "LinkedField",
"name": "fulfillEvidence",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "MitigationViewFulfillEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "FulfillEvidencePayload",
"kind": "LinkedField",
"name": "fulfillEvidence",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "appendEdge",
"key": "",
"kind": "LinkedHandle",
"name": "evidenceEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "147fdc881f797ace12dc1242cab782ad",
"id": null,
"metadata": {},
"name": "MitigationViewFulfillEvidenceMutation",
"operationKind": "mutation",
"text": "mutation MitigationViewFulfillEvidenceMutation(\n $input: FulfillEvidenceInput!\n) {\n fulfillEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n type\n url\n size\n state\n createdAt\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "c0a1e6be43759cfbcd7e25a3b597926a";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<5baf9229b2f520bcfa43960bd6af05e0>>
* @generated SignedSource<<3188657dfa050c818bf0dc06688d7fab>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,7 +9,7 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
export type EvidenceState = "FULFILLED" | "REQUESTED";
export type EvidenceType = "FILE" | "LINK";
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<9d4db1a6dc5ce7a35be41b6f42bc91d2>>
* @generated SignedSource<<6e5efc598a8e6657fd0a1375e6104ff6>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,7 +9,7 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "EXPIRED" | "INVALID" | "VALID";
export type EvidenceState = "FULLFILLED" | "REQUESTED";
export type EvidenceType = "FILE" | "LINK";
export type UploadEvidenceInput = {
description: string;

View File

@@ -29,7 +29,7 @@ import (
type (
Evidence struct {
ID gid.GID `db:"id"`
TaskID *gid.GID `db:"task_id"`
TaskID gid.GID `db:"task_id"`
State EvidenceState `db:"state"`
Type EvidenceType `db:"type"`
ObjectKey string `db:"object_key"`

View File

@@ -24,9 +24,8 @@ type (
)
const (
EvidenceStateValid EvidenceState = iota
EvidenceStateInvalid
EvidenceStateExpired
EvidenceStateRequested EvidenceState = iota
EvidenceStateFulfilled
)
func (es EvidenceState) MarshalText() ([]byte, error) {
@@ -37,12 +36,10 @@ func (es *EvidenceState) UnmarshalText(data []byte) error {
val := string(data)
switch val {
case EvidenceStateValid.String():
*es = EvidenceStateValid
case EvidenceStateInvalid.String():
*es = EvidenceStateInvalid
case EvidenceStateExpired.String():
*es = EvidenceStateExpired
case EvidenceStateRequested.String():
*es = EvidenceStateRequested
case EvidenceStateFulfilled.String():
*es = EvidenceStateFulfilled
default:
return fmt.Errorf("invalid EvidenceState value: %q", val)
}
@@ -54,12 +51,10 @@ func (es EvidenceState) String() string {
var val string
switch es {
case EvidenceStateValid:
val = "VALID"
case EvidenceStateInvalid:
val = "INVALID"
case EvidenceStateExpired:
val = "EXPIRED"
case EvidenceStateRequested:
val = "REQUESTED"
case EvidenceStateFulfilled:
val = "FULFILLED"
}
return val

View File

@@ -0,0 +1,10 @@
CREATE TYPE evidence_state_new AS ENUM ('FULFILLED', 'REQUESTED');
ALTER TABLE evidences ALTER COLUMN state TYPE text;
UPDATE evidences SET state = 'FULFILLED';
ALTER TABLE evidences ALTER COLUMN state TYPE evidence_state_new USING state::evidence_state_new;
DROP TYPE evidence_state;
ALTER TYPE evidence_state_new RENAME TO evidence_state;

View File

@@ -0,0 +1 @@
ALTER TABLE evidences ALTER COLUMN task_id SET NOT NULL;

View File

@@ -61,7 +61,7 @@ LOOP:
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(1 * time.Second):
case <-time.After(60 * time.Second):
ctx := context.Background()
if err := m.batchSendEmails(ctx); err != nil {
m.l.ErrorCtx(ctx, "cannot send email", log.Error(err))

View File

@@ -37,6 +37,19 @@ type (
svc *TenantService
}
RequestEvidenceRequest struct {
TaskID gid.GID
Type coredata.EvidenceType
Name string
Description string
}
FulfilledEvidenceRequest struct {
EvidenceID gid.GID
File io.Reader
URL string
}
CreateEvidenceRequest struct {
TaskID gid.GID
Name string
@@ -67,6 +80,63 @@ func (s EvidenceService) Get(
return evidence, nil
}
func (s EvidenceService) Request(
ctx context.Context,
req RequestEvidenceRequest,
) (*coredata.Evidence, error) {
evidenceID, err := gid.NewGID(s.svc.scope.GetTenantID(), coredata.EvidenceEntityType)
if err != nil {
return nil, fmt.Errorf("cannot create evidence global id: %w", err)
}
now := time.Now()
evidence := &coredata.Evidence{
ID: evidenceID,
TaskID: req.TaskID,
State: coredata.EvidenceStateRequested,
Type: req.Type,
Filename: req.Name,
Description: req.Description,
CreatedAt: now,
UpdatedAt: now,
}
err = s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return evidence.Insert(ctx, conn, s.svc.scope)
},
)
if err != nil {
return nil, fmt.Errorf("cannot insert evidence: %w", err)
}
return evidence, nil
}
func (s EvidenceService) Fulfill(
ctx context.Context,
req FulfilledEvidenceRequest,
) (*coredata.Evidence, error) {
evidence := &coredata.Evidence{}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := evidence.LoadByID(ctx, conn, s.svc.scope, req.EvidenceID); err != nil {
return fmt.Errorf("cannot load evidence: %w", err)
}
evidence.State = coredata.EvidenceStateFulfilled
return evidence.Update(ctx, conn, s.svc.scope)
},
)
}
func (s EvidenceService) Create(
ctx context.Context,
req CreateEvidenceRequest,
@@ -79,8 +149,8 @@ func (s EvidenceService) Create(
evidence := &coredata.Evidence{
ID: evidenceID,
TaskID: &req.TaskID,
State: coredata.EvidenceStateValid,
TaskID: req.TaskID,
State: coredata.EvidenceStateFulfilled,
Type: req.Type,
Filename: req.Name,
URL: req.URL,

View File

@@ -67,15 +67,13 @@ enum TaskState
enum EvidenceState
@goModel(model: "github.com/getprobo/probo/pkg/coredata.EvidenceState") {
VALID
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateValid")
INVALID
FULFILLED
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateInvalid"
value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateFulfilled"
)
EXPIRED
REQUESTED
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateExpired"
value: "github.com/getprobo/probo/pkg/coredata.EvidenceStateRequested"
)
}
@@ -751,7 +749,9 @@ type Mutation {
deleteRiskMapping(input: DeleteRiskMappingInput!): DeleteRiskMappingPayload!
# Evidence mutations
uploadEvidence(input: UploadEvidenceInput!): UploadEvidencePayload!
requestEvidence(input: RequestEvidenceInput!): RequestEvidencePayload!
fulfillEvidence(input: FulfillEvidenceInput!): FulfillEvidencePayload!
createEvidence(input: CreateEvidenceInput!): CreateEvidencePayload!
deleteEvidence(input: DeleteEvidenceInput!): DeleteEvidencePayload!
# Policy mutations
@@ -939,12 +939,25 @@ input DeleteRiskMappingInput {
mitigationId: ID!
}
input UploadEvidenceInput {
input RequestEvidenceInput {
taskId: ID!
name: String!
type: EvidenceType!
description: String!
}
input FulfillEvidenceInput {
evidenceId: ID!
file: Upload
url: String
}
input CreateEvidenceInput {
taskId: ID!
name: String!
type: EvidenceType!
url: String
file: Upload
description: String!
}
@@ -1102,7 +1115,15 @@ type DeleteRiskMappingPayload {
success: Boolean!
}
type UploadEvidencePayload {
type RequestEvidencePayload {
evidenceEdge: EvidenceEdge!
}
type FulfillEvidencePayload {
evidenceEdge: EvidenceEdge!
}
type CreateEvidencePayload {
evidenceEdge: EvidenceEdge!
}

File diff suppressed because it is too large Load Diff

View File

@@ -68,6 +68,19 @@ type CreateControlMappingPayload struct {
Success bool `json:"success"`
}
type CreateEvidenceInput struct {
TaskID gid.GID `json:"taskId"`
Name string `json:"name"`
Type coredata.EvidenceType `json:"type"`
URL *string `json:"url,omitempty"`
File *graphql.Upload `json:"file,omitempty"`
Description string `json:"description"`
}
type CreateEvidencePayload struct {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
}
type CreateFrameworkInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
@@ -306,6 +319,16 @@ type FrameworkEdge struct {
Node *Framework `json:"node"`
}
type FulfillEvidenceInput struct {
EvidenceID gid.GID `json:"evidenceId"`
File *graphql.Upload `json:"file,omitempty"`
URL *string `json:"url,omitempty"`
}
type FulfillEvidencePayload struct {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
}
type ImportFrameworkInput struct {
OrganizationID gid.GID `json:"organizationId"`
File graphql.Upload `json:"file"`
@@ -463,6 +486,17 @@ type RemoveUserPayload struct {
Success bool `json:"success"`
}
type RequestEvidenceInput struct {
TaskID gid.GID `json:"taskId"`
Name string `json:"name"`
Type coredata.EvidenceType `json:"type"`
Description string `json:"description"`
}
type RequestEvidencePayload struct {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
}
type Risk struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
@@ -624,19 +658,6 @@ type UpdateVendorPayload struct {
Vendor *Vendor `json:"vendor"`
}
type UploadEvidenceInput struct {
TaskID gid.GID `json:"taskId"`
Name string `json:"name"`
File *graphql.Upload `json:"file,omitempty"`
Type coredata.EvidenceType `json:"type"`
URL *string `json:"url,omitempty"`
Description string `json:"description"`
}
type UploadEvidencePayload struct {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
}
type User struct {
ID gid.GID `json:"id"`
FullName string `json:"fullName"`

View File

@@ -718,20 +718,62 @@ func (r *mutationResolver) DeleteRiskMapping(ctx context.Context, input types.De
}, nil
}
// UploadEvidence is the resolver for the uploadEvidence field.
func (r *mutationResolver) UploadEvidence(ctx context.Context, input types.UploadEvidenceInput) (*types.UploadEvidencePayload, error) {
// RequestEvidence is the resolver for the requestEvidence field.
func (r *mutationResolver) RequestEvidence(ctx context.Context, input types.RequestEvidenceInput) (*types.RequestEvidencePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
var url string
if input.URL != nil {
url = *input.URL
evidence, err := svc.Evidences.Request(
ctx,
probo.RequestEvidenceRequest{
TaskID: input.TaskID,
Name: input.Name,
Type: input.Type,
Description: input.Description,
},
)
if err != nil {
panic(fmt.Errorf("cannot request evidence: %w", err))
}
return &types.RequestEvidencePayload{
EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt),
}, nil
}
// FulfillEvidence is the resolver for the fulfillEvidence field.
func (r *mutationResolver) FulfillEvidence(ctx context.Context, input types.FulfillEvidenceInput) (*types.FulfillEvidencePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.EvidenceID.TenantID())
req := probo.FulfilledEvidenceRequest{
EvidenceID: input.EvidenceID,
}
if input.File != nil {
req.File = input.File.File
}
if input.URL != nil {
req.URL = *input.URL
}
evidence, err := svc.Evidences.Fulfill(ctx, req)
if err != nil {
panic(fmt.Errorf("cannot fulfill evidence: %w", err))
}
return &types.FulfillEvidencePayload{
EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt),
}, nil
}
// CreateEvidence is the resolver for the createEvidence field.
func (r *mutationResolver) CreateEvidence(ctx context.Context, input types.CreateEvidenceInput) (*types.CreateEvidencePayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
req := probo.CreateEvidenceRequest{
TaskID: input.TaskID,
Name: input.Name,
Type: input.Type,
URL: url,
Description: input.Description,
}
@@ -751,7 +793,7 @@ func (r *mutationResolver) UploadEvidence(ctx context.Context, input types.Uploa
panic(fmt.Errorf("failed to create evidence: %w", err))
}
return &types.UploadEvidencePayload{
return &types.CreateEvidencePayload{
EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt),
}, nil
}