Add mapping between control and mitigation
Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
@@ -137,7 +137,7 @@ function FrameworkViewContent({
|
|||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button variant="outline" asChild>
|
<Button variant="outline" asChild>
|
||||||
<Link
|
<Link
|
||||||
to={`/organizations/${organizationId}/frameworks/${framework.id}/update`}
|
to={`/organizations/${organizationId}/frameworks/${framework.id}/edit`}
|
||||||
>
|
>
|
||||||
Edit Framework
|
Edit Framework
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -3,16 +3,36 @@ import {
|
|||||||
PreloadedQuery,
|
PreloadedQuery,
|
||||||
usePreloadedQuery,
|
usePreloadedQuery,
|
||||||
useQueryLoader,
|
useQueryLoader,
|
||||||
|
useMutation,
|
||||||
|
fetchQuery,
|
||||||
|
useRelayEnvironment,
|
||||||
} from "react-relay";
|
} from "react-relay";
|
||||||
import { ControlViewSkeleton } from "./ControlPage";
|
import { ControlViewSkeleton } from "./ControlPage";
|
||||||
import { Suspense, useEffect } from "react";
|
import { Suspense, useEffect, useState, useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
ControlViewQuery,
|
ControlViewQuery,
|
||||||
ControlViewQuery$data,
|
ControlViewQuery$data,
|
||||||
} from "./__generated__/ControlViewQuery.graphql";
|
} from "./__generated__/ControlViewQuery.graphql";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Link, useParams } from "react-router";
|
import { Link, useParams } from "react-router";
|
||||||
import { Plus } from "lucide-react";
|
import { LinkIcon, X, Loader2, Search } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
const controlViewQuery = graphql`
|
const controlViewQuery = graphql`
|
||||||
query ControlViewQuery($controlId: ID!) {
|
query ControlViewQuery($controlId: ID!) {
|
||||||
@@ -27,15 +47,456 @@ const controlViewQuery = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// New query to fetch linked mitigations
|
||||||
|
const linkedMitigationsQuery = graphql`
|
||||||
|
query ControlViewLinkedMitigationsQuery($controlId: ID!) {
|
||||||
|
control: node(id: $controlId) {
|
||||||
|
id
|
||||||
|
... on Control {
|
||||||
|
mitigations(first: 100) @connection(key: "Control__mitigations") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
importance
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Query to fetch all mitigations for the organization
|
||||||
|
const organizationMitigationsQuery = graphql`
|
||||||
|
query ControlViewOrganizationMitigationsQuery($organizationId: ID!) {
|
||||||
|
organization: node(id: $organizationId) {
|
||||||
|
id
|
||||||
|
... on Organization {
|
||||||
|
mitigations(first: 100) @connection(key: "Organization__mitigations") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
category
|
||||||
|
importance
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Mutation to create a mapping between a control and a mitigation
|
||||||
|
const createMitigationMappingMutation = graphql`
|
||||||
|
mutation ControlViewCreateMitigationMappingMutation(
|
||||||
|
$input: CreateControlMappingInput!
|
||||||
|
) {
|
||||||
|
createControlMapping(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Mutation to delete a mapping between a control and a mitigation
|
||||||
|
const deleteMitigationMappingMutation = graphql`
|
||||||
|
mutation ControlViewDeleteMitigationMappingMutation(
|
||||||
|
$input: DeleteControlMappingInput!
|
||||||
|
) {
|
||||||
|
deleteControlMapping(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add type definitions for the GraphQL responses
|
||||||
|
interface MitigationNode {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
importance: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
|
||||||
|
state: "NOT_STARTED" | "IN_PROGRESS" | "IMPLEMENTED" | "NOT_APPLICABLE";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LinkedMitigationsData {
|
||||||
|
control?: {
|
||||||
|
id: string;
|
||||||
|
mitigations?: {
|
||||||
|
edges: Array<{
|
||||||
|
node: MitigationNode;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrganizationMitigationsData {
|
||||||
|
organization?: {
|
||||||
|
id: string;
|
||||||
|
mitigations?: {
|
||||||
|
edges: Array<{
|
||||||
|
node: MitigationNode;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function Control({
|
export function Control({
|
||||||
control,
|
control,
|
||||||
}: {
|
}: {
|
||||||
control: ControlViewQuery$data["node"];
|
control: ControlViewQuery$data["node"];
|
||||||
}) {
|
}) {
|
||||||
const { organizationId, frameworkId } = useParams<{
|
const { organizationId /* frameworkId */ } = useParams<{
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
frameworkId: string;
|
frameworkId: string;
|
||||||
}>();
|
}>();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const environment = useRelayEnvironment();
|
||||||
|
|
||||||
|
// State for mitigation mapping
|
||||||
|
const [isMitigationMappingDialogOpen, setIsMitigationMappingDialogOpen] =
|
||||||
|
useState(false);
|
||||||
|
const [linkedMitigationsData, setLinkedMitigationsData] =
|
||||||
|
useState<LinkedMitigationsData | null>(null);
|
||||||
|
const [organizationMitigationsData, setOrganizationMitigationsData] =
|
||||||
|
useState<OrganizationMitigationsData | null>(null);
|
||||||
|
const [mitigationSearchQuery, setMitigationSearchQuery] = useState("");
|
||||||
|
const [isLoadingMitigations, setIsLoadingMitigations] = useState(false);
|
||||||
|
const [isLinkingMitigation, setIsLinkingMitigation] = useState(false);
|
||||||
|
const [isUnlinkingMitigation, setIsUnlinkingMitigation] = useState(false);
|
||||||
|
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Create mutation hooks
|
||||||
|
const [commitCreateMitigationMapping] = useMutation(
|
||||||
|
createMitigationMappingMutation
|
||||||
|
);
|
||||||
|
const [commitDeleteMitigationMapping] = useMutation(
|
||||||
|
deleteMitigationMappingMutation
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load initial linked mitigations data
|
||||||
|
useEffect(() => {
|
||||||
|
if (control.id) {
|
||||||
|
setIsLoadingMitigations(true);
|
||||||
|
fetchQuery(environment, linkedMitigationsQuery, {
|
||||||
|
controlId: control.id,
|
||||||
|
}).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedMitigationsData(data as LinkedMitigationsData);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error loading initial mitigations:", error);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [control.id, environment]);
|
||||||
|
|
||||||
|
// Load mitigations data
|
||||||
|
const loadMitigationsData = useCallback(() => {
|
||||||
|
if (!organizationId || !control.id) return;
|
||||||
|
|
||||||
|
setIsLoadingMitigations(true);
|
||||||
|
|
||||||
|
// Fetch all mitigations for the organization
|
||||||
|
fetchQuery(environment, organizationMitigationsQuery, {
|
||||||
|
organizationId,
|
||||||
|
}).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setOrganizationMitigationsData(data as OrganizationMitigationsData);
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
// Fetch linked mitigations for this control
|
||||||
|
fetchQuery(environment, linkedMitigationsQuery, {
|
||||||
|
controlId: control.id,
|
||||||
|
}).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedMitigationsData(data as LinkedMitigationsData);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error fetching linked mitigations:", error);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load linked mitigations.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error fetching organization mitigations:", error);
|
||||||
|
setIsLoadingMitigations(false);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load mitigations.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [control.id, environment, organizationId, toast]);
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const getMitigations = useCallback(() => {
|
||||||
|
if (!organizationMitigationsData?.organization?.mitigations?.edges)
|
||||||
|
return [];
|
||||||
|
return organizationMitigationsData.organization.mitigations.edges.map(
|
||||||
|
(edge) => edge.node
|
||||||
|
);
|
||||||
|
}, [organizationMitigationsData]);
|
||||||
|
|
||||||
|
const getLinkedMitigations = useCallback(() => {
|
||||||
|
if (!linkedMitigationsData?.control?.mitigations?.edges) return [];
|
||||||
|
return linkedMitigationsData.control.mitigations.edges.map(
|
||||||
|
(edge) => edge.node
|
||||||
|
);
|
||||||
|
}, [linkedMitigationsData]);
|
||||||
|
|
||||||
|
const isMitigationLinked = useCallback(
|
||||||
|
(mitigationId: string) => {
|
||||||
|
const linkedMitigations = getLinkedMitigations();
|
||||||
|
return linkedMitigations.some(
|
||||||
|
(mitigation) => mitigation.id === mitigationId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[getLinkedMitigations]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getMitigationCategories = useCallback(() => {
|
||||||
|
const mitigations = getMitigations();
|
||||||
|
const categories = new Set<string>();
|
||||||
|
|
||||||
|
mitigations.forEach((mitigation) => {
|
||||||
|
if (mitigation.category) {
|
||||||
|
categories.add(mitigation.category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(categories).sort();
|
||||||
|
}, [getMitigations]);
|
||||||
|
|
||||||
|
const filteredMitigations = useCallback(() => {
|
||||||
|
const mitigations = getMitigations();
|
||||||
|
if (!mitigationSearchQuery && !categoryFilter) return mitigations;
|
||||||
|
|
||||||
|
return mitigations.filter((mitigation) => {
|
||||||
|
// Filter by search query
|
||||||
|
const matchesSearch =
|
||||||
|
!mitigationSearchQuery ||
|
||||||
|
mitigation.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(mitigationSearchQuery.toLowerCase()) ||
|
||||||
|
(mitigation.description &&
|
||||||
|
mitigation.description
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(mitigationSearchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
// Filter by category
|
||||||
|
const matchesCategory =
|
||||||
|
!categoryFilter ||
|
||||||
|
categoryFilter === "all" ||
|
||||||
|
mitigation.category === categoryFilter;
|
||||||
|
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
});
|
||||||
|
}, [categoryFilter, getMitigations, mitigationSearchQuery]);
|
||||||
|
|
||||||
|
// Handle link/unlink functions
|
||||||
|
const handleLinkMitigation = useCallback(
|
||||||
|
(mitigationId: string) => {
|
||||||
|
if (!control.id) return;
|
||||||
|
|
||||||
|
setIsLinkingMitigation(true);
|
||||||
|
|
||||||
|
commitCreateMitigationMapping({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
controlId: control.id,
|
||||||
|
mitigationId: mitigationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
setIsLinkingMitigation(false);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
console.error("Error linking mitigation:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to link mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh linked mitigations data
|
||||||
|
fetchQuery(environment, linkedMitigationsQuery, {
|
||||||
|
controlId: control.id,
|
||||||
|
}).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedMitigationsData(data as LinkedMitigationsData);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error refreshing linked mitigations:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Mitigation successfully linked to control.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsLinkingMitigation(false);
|
||||||
|
console.error("Error linking mitigation:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to link mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[commitCreateMitigationMapping, control.id, environment, toast]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUnlinkMitigation = useCallback(
|
||||||
|
(mitigationId: string) => {
|
||||||
|
if (!control.id) return;
|
||||||
|
|
||||||
|
setIsUnlinkingMitigation(true);
|
||||||
|
|
||||||
|
commitDeleteMitigationMapping({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
controlId: control.id,
|
||||||
|
mitigationId: mitigationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
setIsUnlinkingMitigation(false);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
console.error("Error unlinking mitigation:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to unlink mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh linked mitigations data
|
||||||
|
fetchQuery(environment, linkedMitigationsQuery, {
|
||||||
|
controlId: control.id,
|
||||||
|
}).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedMitigationsData(data as LinkedMitigationsData);
|
||||||
|
},
|
||||||
|
error: (error: Error) => {
|
||||||
|
console.error("Error refreshing linked mitigations:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Mitigation successfully unlinked from control.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsUnlinkingMitigation(false);
|
||||||
|
console.error("Error unlinking mitigation:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to unlink mitigation. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[commitDeleteMitigationMapping, control.id, environment, toast]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenMitigationMappingDialog = useCallback(() => {
|
||||||
|
loadMitigationsData();
|
||||||
|
setIsMitigationMappingDialogOpen(true);
|
||||||
|
}, [loadMitigationsData]);
|
||||||
|
|
||||||
|
// UI helper functions
|
||||||
|
const formatImportance = (importance: string | undefined): string => {
|
||||||
|
if (!importance) return "Unknown";
|
||||||
|
|
||||||
|
switch (importance) {
|
||||||
|
case "LOW":
|
||||||
|
return "Low";
|
||||||
|
case "MEDIUM":
|
||||||
|
return "Medium";
|
||||||
|
case "HIGH":
|
||||||
|
return "High";
|
||||||
|
case "CRITICAL":
|
||||||
|
return "Critical";
|
||||||
|
default:
|
||||||
|
return importance;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatState = (state: string | undefined): string => {
|
||||||
|
if (!state) return "Unknown";
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case "NOT_STARTED":
|
||||||
|
return "Not Started";
|
||||||
|
case "IN_PROGRESS":
|
||||||
|
return "In Progress";
|
||||||
|
case "IMPLEMENTED":
|
||||||
|
return "Implemented";
|
||||||
|
case "NOT_APPLICABLE":
|
||||||
|
return "Not Applicable";
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImportanceColor = (importance: string | undefined): string => {
|
||||||
|
if (!importance) return "bg-gray-100 text-gray-800";
|
||||||
|
|
||||||
|
switch (importance) {
|
||||||
|
case "LOW":
|
||||||
|
return "bg-blue-100 text-blue-800";
|
||||||
|
case "MEDIUM":
|
||||||
|
return "bg-yellow-100 text-yellow-800";
|
||||||
|
case "HIGH":
|
||||||
|
return "bg-orange-100 text-orange-800";
|
||||||
|
case "CRITICAL":
|
||||||
|
return "bg-red-100 text-red-800";
|
||||||
|
default:
|
||||||
|
return "bg-gray-100 text-gray-800";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStateColor = (state: string | undefined): string => {
|
||||||
|
if (!state) return "bg-gray-100 text-gray-800";
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case "NOT_STARTED":
|
||||||
|
return "bg-gray-100 text-gray-800";
|
||||||
|
case "IN_PROGRESS":
|
||||||
|
return "bg-blue-100 text-blue-800";
|
||||||
|
case "IMPLEMENTED":
|
||||||
|
return "bg-green-100 text-green-800";
|
||||||
|
case "NOT_APPLICABLE":
|
||||||
|
return "bg-purple-100 text-purple-800";
|
||||||
|
default:
|
||||||
|
return "bg-gray-100 text-gray-800";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-auto p-5 flex items-start gap-5">
|
<div className="w-auto p-5 flex items-start gap-5">
|
||||||
@@ -44,22 +505,293 @@ export function Control({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-2xl font-medium">{control.name}</h2>
|
<h2 className="text-2xl font-medium">{control.name}</h2>
|
||||||
<h3 className="text-xl font-medium text-gray-600 mt-8">
|
|
||||||
Security measures
|
{/* Control Description */}
|
||||||
</h3>
|
{control.description && (
|
||||||
<p className="mt-4">
|
<div className="mt-4 text-gray-600">{control.description}</div>
|
||||||
Security measures will be displayed here once connected to this
|
)}
|
||||||
control
|
|
||||||
</p>
|
{/* Security Measures Section */}
|
||||||
<div className="mt-2">
|
<div className="mt-8">
|
||||||
<Button asChild>
|
{/* Mitigation Mapping Dialog */}
|
||||||
<Link
|
<Dialog
|
||||||
to={`/organizations/${organizationId}/frameworks/${frameworkId}/mitigations/create`}
|
open={isMitigationMappingDialogOpen}
|
||||||
>
|
onOpenChange={setIsMitigationMappingDialogOpen}
|
||||||
<Plus className="h-3 w-3 mr-1" />
|
>
|
||||||
Link Security Measures
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
</Link>
|
<DialogHeader>
|
||||||
</Button>
|
<DialogTitle>Link Security Measures to Control</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Search and select security measures to link to this control.
|
||||||
|
This helps track which security measures address this control.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4 mb-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search security measures by name or description..."
|
||||||
|
value={mitigationSearchQuery}
|
||||||
|
onChange={(e) => setMitigationSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-[200px]">
|
||||||
|
<Select
|
||||||
|
value={categoryFilter || "all"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setCategoryFilter(value === "all" ? null : value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="All categories" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All categories</SelectItem>
|
||||||
|
{getMitigationCategories().map((category) => (
|
||||||
|
<SelectItem key={category} value={category}>
|
||||||
|
{category}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{isLoadingMitigations ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
|
||||||
|
<span className="ml-2">Loading security measures...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="max-h-[50vh] overflow-y-auto pr-2">
|
||||||
|
{filteredMitigations().length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
No security measures found. Try adjusting your search or
|
||||||
|
select a different category.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="sticky top-0 bg-white">
|
||||||
|
<tr className="border-b text-left text-sm text-gray-500 bg-gray-50">
|
||||||
|
<th className="py-3 px-4 font-medium">Name</th>
|
||||||
|
<th className="py-3 px-4 font-medium">
|
||||||
|
Importance
|
||||||
|
</th>
|
||||||
|
<th className="py-3 px-4 font-medium">State</th>
|
||||||
|
<th className="py-3 px-4 font-medium text-right">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredMitigations().map(
|
||||||
|
(mitigation: MitigationNode) => {
|
||||||
|
const isLinked = isMitigationLinked(
|
||||||
|
mitigation.id
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={mitigation.id}
|
||||||
|
className="border-b hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div className="font-medium">
|
||||||
|
{mitigation.name}
|
||||||
|
</div>
|
||||||
|
{mitigation.description && (
|
||||||
|
<div className="text-xs text-gray-500 line-clamp-1 mt-0.5">
|
||||||
|
{mitigation.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${getImportanceColor(
|
||||||
|
mitigation.importance
|
||||||
|
)} inline-block`}
|
||||||
|
>
|
||||||
|
{formatImportance(mitigation.importance)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
|
||||||
|
mitigation.state
|
||||||
|
)} inline-block`}
|
||||||
|
>
|
||||||
|
{formatState(mitigation.state)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 text-right whitespace-nowrap">
|
||||||
|
{isLinked ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleUnlinkMitigation(mitigation.id)
|
||||||
|
}
|
||||||
|
disabled={isUnlinkingMitigation}
|
||||||
|
className="text-xs h-7 text-red-500 border-red-200 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
{isUnlinkingMitigation ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1">Unlink</span>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleLinkMitigation(mitigation.id)
|
||||||
|
}
|
||||||
|
disabled={isLinkingMitigation}
|
||||||
|
className="text-xs h-7 text-blue-500 border-blue-200 hover:bg-blue-50"
|
||||||
|
>
|
||||||
|
{isLinkingMitigation ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<LinkIcon className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1">Link</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="mt-4">
|
||||||
|
<Button onClick={() => setIsMitigationMappingDialogOpen(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Linked Mitigations List */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-xl font-medium text-gray-600">
|
||||||
|
Security measures
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
onClick={handleOpenMitigationMappingDialog}
|
||||||
|
>
|
||||||
|
<LinkIcon className="w-4 h-4" />
|
||||||
|
<span>Link Security Measures</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoadingMitigations ? (
|
||||||
|
<div className="flex items-center justify-center h-24">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-blue-500" />
|
||||||
|
<span className="ml-2">Loading security measures...</span>
|
||||||
|
</div>
|
||||||
|
) : linkedMitigationsData?.control?.mitigations?.edges &&
|
||||||
|
linkedMitigationsData.control.mitigations.edges.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto border rounded-md">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b text-left text-sm text-gray-500 bg-gray-50">
|
||||||
|
<th className="py-3 px-4 font-medium">Name</th>
|
||||||
|
<th className="py-3 px-4 font-medium">Importance</th>
|
||||||
|
<th className="py-3 px-4 font-medium">State</th>
|
||||||
|
<th className="py-3 px-4 font-medium text-right">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{getLinkedMitigations().map(
|
||||||
|
(mitigation: MitigationNode) => (
|
||||||
|
<tr
|
||||||
|
key={mitigation.id}
|
||||||
|
className="border-b hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div className="font-medium">{mitigation.name}</div>
|
||||||
|
{mitigation.description && (
|
||||||
|
<div className="text-xs text-gray-500 line-clamp-1 mt-0.5">
|
||||||
|
{mitigation.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${getImportanceColor(
|
||||||
|
mitigation.importance
|
||||||
|
)} inline-block`}
|
||||||
|
>
|
||||||
|
{formatImportance(mitigation.importance)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs ${getStateColor(
|
||||||
|
mitigation.state
|
||||||
|
)} inline-block`}
|
||||||
|
>
|
||||||
|
{formatState(mitigation.state)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 text-right whitespace-nowrap">
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
asChild
|
||||||
|
className="text-xs h-7"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={`/organizations/${organizationId}/mitigations/${mitigation.id}`}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleUnlinkMitigation(mitigation.id)
|
||||||
|
}
|
||||||
|
disabled={isUnlinkingMitigation}
|
||||||
|
className="text-xs h-7 text-red-500 border-red-200 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
Unlink
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500 border rounded-md">
|
||||||
|
No security measures linked to this control yet. Click
|
||||||
|
"Link Security Measures" to connect some.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<02b5ad33ae08fb81731cf27c34f31ccf>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type CreateControlMappingInput = {
|
||||||
|
controlId: string;
|
||||||
|
mitigationId: string;
|
||||||
|
};
|
||||||
|
export type ControlViewCreateMitigationMappingMutation$variables = {
|
||||||
|
input: CreateControlMappingInput;
|
||||||
|
};
|
||||||
|
export type ControlViewCreateMitigationMappingMutation$data = {
|
||||||
|
readonly createControlMapping: {
|
||||||
|
readonly success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ControlViewCreateMitigationMappingMutation = {
|
||||||
|
response: ControlViewCreateMitigationMappingMutation$data;
|
||||||
|
variables: ControlViewCreateMitigationMappingMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "CreateControlMappingPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "createControlMapping",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "success",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ControlViewCreateMitigationMappingMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ControlViewCreateMitigationMappingMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "1d95423714868543cd1ae0867f7274f5",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ControlViewCreateMitigationMappingMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation ControlViewCreateMitigationMappingMutation(\n $input: CreateControlMappingInput!\n) {\n createControlMapping(input: $input) {\n success\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "caa36b4928fc747295d0b3cb8c90f786";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<c4c2d951d675bc0a763fcf90b6a9a959>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type DeleteControlMappingInput = {
|
||||||
|
controlId: string;
|
||||||
|
mitigationId: string;
|
||||||
|
};
|
||||||
|
export type ControlViewDeleteMitigationMappingMutation$variables = {
|
||||||
|
input: DeleteControlMappingInput;
|
||||||
|
};
|
||||||
|
export type ControlViewDeleteMitigationMappingMutation$data = {
|
||||||
|
readonly deleteControlMapping: {
|
||||||
|
readonly success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ControlViewDeleteMitigationMappingMutation = {
|
||||||
|
response: ControlViewDeleteMitigationMappingMutation$data;
|
||||||
|
variables: ControlViewDeleteMitigationMappingMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "DeleteControlMappingPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "deleteControlMapping",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "success",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ControlViewDeleteMitigationMappingMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ControlViewDeleteMitigationMappingMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "49d66f6dddce3b6c7a82334e36d9b464",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ControlViewDeleteMitigationMappingMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation ControlViewDeleteMitigationMappingMutation(\n $input: DeleteControlMappingInput!\n) {\n deleteControlMapping(input: $input) {\n success\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "6f954d3c3c2b38b68a0be5f51de3b935";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<5a15ae55cd27e88ee96715ba785e412d>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||||
|
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
|
export type ControlViewLinkedMitigationsQuery$variables = {
|
||||||
|
controlId: string;
|
||||||
|
};
|
||||||
|
export type ControlViewLinkedMitigationsQuery$data = {
|
||||||
|
readonly control: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly mitigations?: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly category: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly importance: MitigationImportance;
|
||||||
|
readonly name: string;
|
||||||
|
readonly state: MitigationState;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ControlViewLinkedMitigationsQuery = {
|
||||||
|
response: ControlViewLinkedMitigationsQuery$data;
|
||||||
|
variables: ControlViewLinkedMitigationsQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "controlId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "id",
|
||||||
|
"variableName": "controlId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v2 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "__typename",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v4 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MitigationEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Mitigation",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "description",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "category",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "importance",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "state",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "cursor",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "PageInfo",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "pageInfo",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "endCursor",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "hasNextPage",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v5 = [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ControlViewLinkedMitigationsQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "control",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "mitigations",
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MitigationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__Control__mitigations_connection",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v4/*: any*/),
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Control",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ControlViewLinkedMitigationsQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "control",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"concreteType": "MitigationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "mitigations",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v4/*: any*/),
|
||||||
|
"storageKey": "mitigations(first:100)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"filters": null,
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "Control__mitigations",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "mitigations"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Control",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "909a089ca2452cc35928267d86d1bb7b",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {
|
||||||
|
"connection": [
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"cursor": null,
|
||||||
|
"direction": "forward",
|
||||||
|
"path": [
|
||||||
|
"control",
|
||||||
|
"mitigations"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "ControlViewLinkedMitigationsQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query ControlViewLinkedMitigationsQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n importance\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "c89fb8a01e18c89d2ae310f4c7a2b522";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<a41b64671a286465fbaa69551d5ec1c8>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
|
||||||
|
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
|
||||||
|
export type ControlViewOrganizationMitigationsQuery$variables = {
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type ControlViewOrganizationMitigationsQuery$data = {
|
||||||
|
readonly organization: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly mitigations?: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly category: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly importance: MitigationImportance;
|
||||||
|
readonly name: string;
|
||||||
|
readonly state: MitigationState;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ControlViewOrganizationMitigationsQuery = {
|
||||||
|
response: ControlViewOrganizationMitigationsQuery$data;
|
||||||
|
variables: ControlViewOrganizationMitigationsQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "organizationId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "id",
|
||||||
|
"variableName": "organizationId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v2 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "__typename",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v4 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MitigationEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Mitigation",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "description",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "category",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "importance",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "state",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "cursor",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "PageInfo",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "pageInfo",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "endCursor",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "hasNextPage",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v5 = [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ControlViewOrganizationMitigationsQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "organization",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "mitigations",
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MitigationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__Organization__mitigations_connection",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v4/*: any*/),
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ControlViewOrganizationMitigationsQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "organization",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"concreteType": "MitigationConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "mitigations",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v4/*: any*/),
|
||||||
|
"storageKey": "mitigations(first:100)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"filters": null,
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "Organization__mitigations",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "mitigations"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "f5c0ad1c64f05306ed6c23a5ad4d649e",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {
|
||||||
|
"connection": [
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"cursor": null,
|
||||||
|
"direction": "forward",
|
||||||
|
"path": [
|
||||||
|
"organization",
|
||||||
|
"mitigations"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "ControlViewOrganizationMitigationsQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query ControlViewOrganizationMitigationsQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n importance\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "5c14bf02367f0b6e5ccc633b7a77d1aa";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -34,6 +34,8 @@ import {
|
|||||||
UserMinus,
|
UserMinus,
|
||||||
User,
|
User,
|
||||||
Link2,
|
Link2,
|
||||||
|
Search,
|
||||||
|
Link as LinkIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -58,6 +60,7 @@ import {
|
|||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
@@ -70,6 +73,7 @@ import {
|
|||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetClose,
|
SheetClose,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
import { PageTemplate } from "@/components/PageTemplate";
|
import { PageTemplate } from "@/components/PageTemplate";
|
||||||
import { MitigationViewSkeleton } from "./MitigationPage";
|
import { MitigationViewSkeleton } from "./MitigationPage";
|
||||||
@@ -83,6 +87,10 @@ import { MitigationViewUnassignTaskMutation as MitigationViewUnassignTaskMutatio
|
|||||||
import { MitigationViewUpdateMitigationStateMutation as MitigationViewUpdateMitigationStateMutationType } from "./__generated__/MitigationViewUpdateMitigationStateMutation.graphql";
|
import { MitigationViewUpdateMitigationStateMutation as MitigationViewUpdateMitigationStateMutationType } from "./__generated__/MitigationViewUpdateMitigationStateMutation.graphql";
|
||||||
import { MitigationViewQuery as MitigationViewQueryType } from "./__generated__/MitigationViewQuery.graphql";
|
import { MitigationViewQuery as MitigationViewQueryType } from "./__generated__/MitigationViewQuery.graphql";
|
||||||
import { MitigationViewOrganizationQuery$data } from "./__generated__/MitigationViewOrganizationQuery.graphql";
|
import { MitigationViewOrganizationQuery$data } from "./__generated__/MitigationViewOrganizationQuery.graphql";
|
||||||
|
import { MitigationViewFrameworksQuery$data } from "./__generated__/MitigationViewFrameworksQuery.graphql";
|
||||||
|
import { MitigationViewLinkedControlsQuery$data } from "./__generated__/MitigationViewLinkedControlsQuery.graphql";
|
||||||
|
import { MitigationViewCreateControlMappingMutation$data } from "./__generated__/MitigationViewCreateControlMappingMutation.graphql";
|
||||||
|
import { MitigationViewDeleteControlMappingMutation$data } from "./__generated__/MitigationViewDeleteControlMappingMutation.graphql";
|
||||||
|
|
||||||
// Function to format ISO8601 duration to human-readable format
|
// Function to format ISO8601 duration to human-readable format
|
||||||
const formatDuration = (isoDuration: string): string => {
|
const formatDuration = (isoDuration: string): string => {
|
||||||
@@ -325,6 +333,75 @@ const organizationQuery = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// New queries and mutations for Control Mapping
|
||||||
|
const frameworksQuery = graphql`
|
||||||
|
query MitigationViewFrameworksQuery($organizationId: ID!) {
|
||||||
|
organization: node(id: $organizationId) {
|
||||||
|
id
|
||||||
|
... on Organization {
|
||||||
|
frameworks(first: 100) @connection(key: "Organization__frameworks") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
controls(first: 100) @connection(key: "Framework__controls") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
referenceId
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const linkedControlsQuery = graphql`
|
||||||
|
query MitigationViewLinkedControlsQuery($mitigationId: ID!) {
|
||||||
|
mitigation: node(id: $mitigationId) {
|
||||||
|
id
|
||||||
|
... on Mitigation {
|
||||||
|
controls(first: 100) @connection(key: "Mitigation__controls") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
referenceId
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const createControlMappingMutation = graphql`
|
||||||
|
mutation MitigationViewCreateControlMappingMutation(
|
||||||
|
$input: CreateControlMappingInput!
|
||||||
|
) {
|
||||||
|
createControlMapping(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const deleteControlMappingMutation = graphql`
|
||||||
|
mutation MitigationViewDeleteControlMappingMutation(
|
||||||
|
$input: DeleteControlMappingInput!
|
||||||
|
) {
|
||||||
|
deleteControlMapping(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
function MitigationViewContent({
|
function MitigationViewContent({
|
||||||
queryRef,
|
queryRef,
|
||||||
}: {
|
}: {
|
||||||
@@ -347,25 +424,55 @@ function MitigationViewContent({
|
|||||||
const [organizationData, setOrganizationData] =
|
const [organizationData, setOrganizationData] =
|
||||||
useState<MitigationViewOrganizationQuery$data | null>(null);
|
useState<MitigationViewOrganizationQuery$data | null>(null);
|
||||||
|
|
||||||
|
// Control mapping state
|
||||||
|
const [isControlMappingDialogOpen, setIsControlMappingDialogOpen] =
|
||||||
|
useState(false);
|
||||||
|
const [frameworksData, setFrameworksData] = useState<any | null>(null);
|
||||||
|
const [linkedControlsData, setLinkedControlsData] = useState<any | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [controlSearchQuery, setControlSearchQuery] = useState("");
|
||||||
|
const [selectedFrameworkId, setSelectedFrameworkId] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [isLoadingControls, setIsLoadingControls] = useState(false);
|
||||||
|
const [isLinkingControl, setIsLinkingControl] = useState(false);
|
||||||
|
const [isUnlinkingControl, setIsUnlinkingControl] = useState(false);
|
||||||
|
|
||||||
|
// Create mutation hooks for control mapping
|
||||||
|
const [commitCreateControlMapping] = useMutation(
|
||||||
|
createControlMappingMutation
|
||||||
|
);
|
||||||
|
const [commitDeleteControlMapping] = useMutation(
|
||||||
|
deleteControlMappingMutation
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
fetchQuery(environment, organizationQuery, {
|
fetchQuery(environment, organizationQuery, { organizationId }).subscribe({
|
||||||
organizationId,
|
next: (data) => {
|
||||||
})
|
setOrganizationData(data);
|
||||||
.toPromise()
|
},
|
||||||
.then((response) => {
|
error: (error) => {
|
||||||
setOrganizationData(response as MitigationViewOrganizationQuery$data);
|
console.error("Error fetching organization:", error);
|
||||||
})
|
},
|
||||||
.catch((error) => {
|
});
|
||||||
console.error("Error fetching organization data:", error);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to load people data",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [organizationId, environment, toast]);
|
}, [environment, organizationId]);
|
||||||
|
|
||||||
|
// Load linked controls when component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
if (mitigationId) {
|
||||||
|
fetchQuery(environment, linkedControlsQuery, { mitigationId }).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
setLinkedControlsData(data);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error("Error fetching linked controls:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [environment, mitigationId]);
|
||||||
|
|
||||||
const formatImportance = (importance: string | undefined): string => {
|
const formatImportance = (importance: string | undefined): string => {
|
||||||
if (!importance) return "";
|
if (!importance) return "";
|
||||||
@@ -1123,9 +1230,9 @@ function MitigationViewContent({
|
|||||||
|
|
||||||
// Update SheetContent to handle closing
|
// Update SheetContent to handle closing
|
||||||
const handleCloseTaskPanel = () => {
|
const handleCloseTaskPanel = () => {
|
||||||
|
setSelectedTask(null);
|
||||||
setIsTaskPanelOpen(false);
|
setIsTaskPanelOpen(false);
|
||||||
|
// Remove taskId from URL when panel is closed
|
||||||
// Remove the task ID from URL parameters when closing
|
|
||||||
searchParams.delete("taskId");
|
searchParams.delete("taskId");
|
||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
};
|
};
|
||||||
@@ -1242,6 +1349,227 @@ function MitigationViewContent({
|
|||||||
[parseISODuration]
|
[parseISODuration]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Control mapping functions
|
||||||
|
const loadFrameworksAndControls = useCallback(() => {
|
||||||
|
if (!organizationId || !mitigationId) return;
|
||||||
|
|
||||||
|
setIsLoadingControls(true);
|
||||||
|
|
||||||
|
// Fetch all frameworks and their controls
|
||||||
|
fetchQuery(environment, frameworksQuery, { organizationId }).subscribe({
|
||||||
|
next: (data: any) => {
|
||||||
|
setFrameworksData(data);
|
||||||
|
if (
|
||||||
|
data?.organization?.frameworks?.edges?.length > 0 &&
|
||||||
|
!selectedFrameworkId
|
||||||
|
) {
|
||||||
|
// Select the first framework by default if none is selected
|
||||||
|
setSelectedFrameworkId(data.organization.frameworks.edges[0].node.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
// Fetch already linked controls for this mitigation
|
||||||
|
fetchQuery(environment, linkedControlsQuery, {
|
||||||
|
mitigationId,
|
||||||
|
}).subscribe({
|
||||||
|
next: (data: any) => {
|
||||||
|
setLinkedControlsData(data);
|
||||||
|
setIsLoadingControls(false);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error("Error fetching linked controls:", error);
|
||||||
|
setIsLoadingControls(false);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load linked controls.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error("Error fetching frameworks:", error);
|
||||||
|
setIsLoadingControls(false);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load frameworks and controls.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [environment, mitigationId, organizationId, selectedFrameworkId, toast]);
|
||||||
|
|
||||||
|
const getControls = useCallback(() => {
|
||||||
|
if (!frameworksData?.organization?.frameworks?.edges) return [];
|
||||||
|
|
||||||
|
// Get controls from the selected framework
|
||||||
|
const frameworks = frameworksData.organization.frameworks.edges;
|
||||||
|
if (selectedFrameworkId) {
|
||||||
|
const selectedFramework = frameworks.find(
|
||||||
|
(edge: any) => edge.node.id === selectedFrameworkId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedFramework?.node?.controls?.edges) {
|
||||||
|
return selectedFramework.node.controls.edges.map(
|
||||||
|
(edge: any) => edge.node
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no framework is selected or it doesn't have controls, return controls from all frameworks
|
||||||
|
return frameworks.flatMap((framework: any) =>
|
||||||
|
framework.node.controls.edges.map((edge: any) => edge.node)
|
||||||
|
);
|
||||||
|
}, [frameworksData, selectedFrameworkId]);
|
||||||
|
|
||||||
|
const getLinkedControls = useCallback(() => {
|
||||||
|
if (!linkedControlsData?.mitigation?.controls?.edges) return [];
|
||||||
|
return linkedControlsData.mitigation.controls.edges.map(
|
||||||
|
(edge) => edge.node
|
||||||
|
);
|
||||||
|
}, [linkedControlsData]);
|
||||||
|
|
||||||
|
const isControlLinked = useCallback(
|
||||||
|
(controlId: string) => {
|
||||||
|
const linkedControls = getLinkedControls();
|
||||||
|
return linkedControls.some((control: any) => control.id === controlId);
|
||||||
|
},
|
||||||
|
[getLinkedControls]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLinkControl = useCallback(
|
||||||
|
(controlId: string) => {
|
||||||
|
if (!mitigationId) return;
|
||||||
|
|
||||||
|
setIsLinkingControl(true);
|
||||||
|
|
||||||
|
commitCreateControlMapping({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
controlId,
|
||||||
|
mitigationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
setIsLinkingControl(false);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
console.error("Error linking control:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to link control. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh linked controls data
|
||||||
|
fetchQuery(environment, linkedControlsQuery, {
|
||||||
|
mitigationId,
|
||||||
|
}).subscribe({
|
||||||
|
next: (data: any) => {
|
||||||
|
setLinkedControlsData(data);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error("Error refreshing linked controls:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Control successfully linked to mitigation.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsLinkingControl(false);
|
||||||
|
console.error("Error linking control:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to link control. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[commitCreateControlMapping, environment, mitigationId, toast]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUnlinkControl = useCallback(
|
||||||
|
(controlId: string) => {
|
||||||
|
if (!mitigationId) return;
|
||||||
|
|
||||||
|
setIsUnlinkingControl(true);
|
||||||
|
|
||||||
|
commitDeleteControlMapping({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
controlId,
|
||||||
|
mitigationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
setIsUnlinkingControl(false);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
console.error("Error unlinking control:", errors);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to unlink control. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh linked controls data
|
||||||
|
fetchQuery(environment, linkedControlsQuery, {
|
||||||
|
mitigationId,
|
||||||
|
}).subscribe({
|
||||||
|
next: (data: any) => {
|
||||||
|
setLinkedControlsData(data);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error("Error refreshing linked controls:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Control successfully unlinked from mitigation.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setIsUnlinkingControl(false);
|
||||||
|
console.error("Error unlinking control:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to unlink control. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[commitDeleteControlMapping, environment, mitigationId, toast]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenControlMappingDialog = useCallback(() => {
|
||||||
|
loadFrameworksAndControls();
|
||||||
|
setIsControlMappingDialogOpen(true);
|
||||||
|
}, [loadFrameworksAndControls]);
|
||||||
|
|
||||||
|
const filteredControls = useCallback(() => {
|
||||||
|
const controls = getControls();
|
||||||
|
if (!controlSearchQuery) return controls;
|
||||||
|
|
||||||
|
const lowerQuery = controlSearchQuery.toLowerCase();
|
||||||
|
return controls.filter(
|
||||||
|
(control: any) =>
|
||||||
|
control.referenceId.toLowerCase().includes(lowerQuery) ||
|
||||||
|
control.name.toLowerCase().includes(lowerQuery) ||
|
||||||
|
(control.description &&
|
||||||
|
control.description.toLowerCase().includes(lowerQuery))
|
||||||
|
);
|
||||||
|
}, [controlSearchQuery, getControls]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTemplate
|
<PageTemplate
|
||||||
title={data.mitigation.name ?? ""}
|
title={data.mitigation.name ?? ""}
|
||||||
@@ -1290,6 +1618,217 @@ function MitigationViewContent({
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Control Mapping Section */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-semibold">Controls</h2>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
onClick={handleOpenControlMappingDialog}
|
||||||
|
>
|
||||||
|
<LinkIcon className="w-4 h-4" />
|
||||||
|
<span>Map to Controls</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Control Mapping Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={isControlMappingDialogOpen}
|
||||||
|
onOpenChange={setIsControlMappingDialogOpen}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Map Mitigation to Controls</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Search and select controls to link to this mitigation. This
|
||||||
|
helps track which controls are addressed by this mitigation.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4 mb-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search controls by ID, name, or description..."
|
||||||
|
value={controlSearchQuery}
|
||||||
|
onChange={(e) => setControlSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-64">
|
||||||
|
<Select
|
||||||
|
value={selectedFrameworkId || "all"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setSelectedFrameworkId(value === "all" ? null : value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select framework" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Frameworks</SelectItem>
|
||||||
|
{frameworksData?.organization?.frameworks?.edges?.map(
|
||||||
|
(edge: any) => (
|
||||||
|
<SelectItem key={edge.node.id} value={edge.node.id}>
|
||||||
|
{edge.node.name}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{isLoadingControls ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
|
||||||
|
<span className="ml-2">Loading controls...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 gap-2 max-h-[50vh] overflow-y-auto pr-2">
|
||||||
|
{filteredControls().length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
No controls found. Try adjusting your search or select a
|
||||||
|
different framework.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredControls().map((control: any) => {
|
||||||
|
const isLinked = isControlLinked(control.id);
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={control.id}
|
||||||
|
className="border overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`p-4 ${isLinked ? "bg-blue-50" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className="font-mono text-sm px-1 py-0.5 rounded-sm bg-lime-100 border border-lime-200 text-lime-800 font-bold">
|
||||||
|
{control.referenceId}
|
||||||
|
</div>
|
||||||
|
{isLinked && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-blue-100 text-blue-800 border-blue-200"
|
||||||
|
>
|
||||||
|
Linked
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h3 className="font-medium">{control.name}</h3>
|
||||||
|
{control.description && (
|
||||||
|
<p className="text-sm text-gray-500 mt-1 line-clamp-2">
|
||||||
|
{control.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
{isLinked ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleUnlinkControl(control.id)
|
||||||
|
}
|
||||||
|
disabled={isUnlinkingControl}
|
||||||
|
className="text-red-500 border-red-200 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
{isUnlinkingControl ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1">Unlink</span>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleLinkControl(control.id)
|
||||||
|
}
|
||||||
|
disabled={isLinkingControl}
|
||||||
|
className="text-blue-500 border-blue-200 hover:bg-blue-50"
|
||||||
|
>
|
||||||
|
{isLinkingControl ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<LinkIcon className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1">Link</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="mt-4">
|
||||||
|
<Button onClick={() => setIsControlMappingDialogOpen(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Linked Controls List */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
{linkedControlsData?.mitigation?.controls?.edges?.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{getLinkedControls().map((control: any) => (
|
||||||
|
<Card key={control.id} className="border overflow-hidden">
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className="font-mono text-sm px-1 py-0.5 rounded-sm bg-lime-100 border border-lime-200 text-lime-800 font-bold">
|
||||||
|
{control.referenceId}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-medium text-sm">{control.name}</h3>
|
||||||
|
{control.description && (
|
||||||
|
<p className="text-xs text-gray-500 mt-1 line-clamp-2">
|
||||||
|
{control.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end mt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleUnlinkControl(control.id)}
|
||||||
|
disabled={isUnlinkingControl}
|
||||||
|
className="text-sm h-7 text-red-500 border-red-200 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
Unlink
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
No controls linked to this mitigation yet. Click "Map to
|
||||||
|
Controls" to link controls.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h2 className="text-xl font-semibold">Tasks</h2>
|
<h2 className="text-xl font-semibold">Tasks</h2>
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<d072ba12f8b6bd81f47e8d2fbe56db9c>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type CreateControlMappingInput = {
|
||||||
|
controlId: string;
|
||||||
|
mitigationId: string;
|
||||||
|
};
|
||||||
|
export type MitigationViewCreateControlMappingMutation$variables = {
|
||||||
|
input: CreateControlMappingInput;
|
||||||
|
};
|
||||||
|
export type MitigationViewCreateControlMappingMutation$data = {
|
||||||
|
readonly createControlMapping: {
|
||||||
|
readonly success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type MitigationViewCreateControlMappingMutation = {
|
||||||
|
response: MitigationViewCreateControlMappingMutation$data;
|
||||||
|
variables: MitigationViewCreateControlMappingMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "CreateControlMappingPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "createControlMapping",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "success",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "MitigationViewCreateControlMappingMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "MitigationViewCreateControlMappingMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "79290e466d2aa856113aae8a4e7a0035",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "MitigationViewCreateControlMappingMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation MitigationViewCreateControlMappingMutation(\n $input: CreateControlMappingInput!\n) {\n createControlMapping(input: $input) {\n success\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "32a27dc5fdd06c261258e80db3db50fc";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<aee67baf82965259c299a4ae9947bda3>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type DeleteControlMappingInput = {
|
||||||
|
controlId: string;
|
||||||
|
mitigationId: string;
|
||||||
|
};
|
||||||
|
export type MitigationViewDeleteControlMappingMutation$variables = {
|
||||||
|
input: DeleteControlMappingInput;
|
||||||
|
};
|
||||||
|
export type MitigationViewDeleteControlMappingMutation$data = {
|
||||||
|
readonly deleteControlMapping: {
|
||||||
|
readonly success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type MitigationViewDeleteControlMappingMutation = {
|
||||||
|
response: MitigationViewDeleteControlMappingMutation$data;
|
||||||
|
variables: MitigationViewDeleteControlMappingMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "DeleteControlMappingPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "deleteControlMapping",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "success",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "MitigationViewDeleteControlMappingMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "MitigationViewDeleteControlMappingMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "824394c9cc09d7d428ad42835d3737ef",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "MitigationViewDeleteControlMappingMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation MitigationViewDeleteControlMappingMutation(\n $input: DeleteControlMappingInput!\n) {\n deleteControlMapping(input: $input) {\n success\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "4d7bd098ed16e825abec0e5d996b06e9";
|
||||||
|
|
||||||
|
export default node;
|
||||||
364
apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewFrameworksQuery.graphql.ts
generated
Normal file
364
apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewFrameworksQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<641911581864265e2933d2d5f2705bfe>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type MitigationViewFrameworksQuery$variables = {
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type MitigationViewFrameworksQuery$data = {
|
||||||
|
readonly organization: {
|
||||||
|
readonly frameworks?: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly controls: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly description: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly referenceId: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
readonly id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type MitigationViewFrameworksQuery = {
|
||||||
|
response: MitigationViewFrameworksQuery$data;
|
||||||
|
variables: MitigationViewFrameworksQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "organizationId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "id",
|
||||||
|
"variableName": "organizationId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v2 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v4 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "__typename",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v5 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "cursor",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v6 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "PageInfo",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "pageInfo",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "endCursor",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "hasNextPage",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v7 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "ControlEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Control",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "referenceId",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "description",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v4/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v5/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v6/*: any*/)
|
||||||
|
],
|
||||||
|
v8 = [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "MitigationViewFrameworksQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "organization",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "frameworks",
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "FrameworkConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__Organization__frameworks_connection",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "FrameworkEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Framework",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": "controls",
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "ControlConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__Framework__controls_connection",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v7/*: any*/),
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v4/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v5/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v6/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "MitigationViewFrameworksQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "organization",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v4/*: any*/),
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v8/*: any*/),
|
||||||
|
"concreteType": "FrameworkConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "frameworks",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "FrameworkEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Framework",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v8/*: any*/),
|
||||||
|
"concreteType": "ControlConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "controls",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v7/*: any*/),
|
||||||
|
"storageKey": "controls(first:100)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v8/*: any*/),
|
||||||
|
"filters": null,
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "Framework__controls",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "controls"
|
||||||
|
},
|
||||||
|
(v4/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v5/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v6/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": "frameworks(first:100)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v8/*: any*/),
|
||||||
|
"filters": null,
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "Organization__frameworks",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "frameworks"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "34e6669a992e649351d4284a2d66c66a",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {
|
||||||
|
"connection": [
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"cursor": null,
|
||||||
|
"direction": "forward",
|
||||||
|
"path": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"cursor": null,
|
||||||
|
"direction": "forward",
|
||||||
|
"path": [
|
||||||
|
"organization",
|
||||||
|
"frameworks"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "MitigationViewFrameworksQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query MitigationViewFrameworksQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n frameworks(first: 100) {\n edges {\n node {\n id\n name\n controls(first: 100) {\n edges {\n node {\n id\n referenceId\n name\n description\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "f4f9ed0abc3a0d36365801dcc60c4333";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<c9c1170d3b2dcef033fe6f69b6a9b11e>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type MitigationViewLinkedControlsQuery$variables = {
|
||||||
|
mitigationId: string;
|
||||||
|
};
|
||||||
|
export type MitigationViewLinkedControlsQuery$data = {
|
||||||
|
readonly mitigation: {
|
||||||
|
readonly controls?: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly description: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly referenceId: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
readonly id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type MitigationViewLinkedControlsQuery = {
|
||||||
|
response: MitigationViewLinkedControlsQuery$data;
|
||||||
|
variables: MitigationViewLinkedControlsQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "mitigationId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "id",
|
||||||
|
"variableName": "mitigationId"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v2 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "__typename",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v4 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "ControlEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Control",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "referenceId",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "description",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "cursor",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "PageInfo",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "pageInfo",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "endCursor",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "hasNextPage",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v5 = [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "MitigationViewLinkedControlsQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "mitigation",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "controls",
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "ControlConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__Mitigation__controls_connection",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v4/*: any*/),
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Mitigation",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "MitigationViewLinkedControlsQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "mitigation",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"concreteType": "ControlConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "controls",
|
||||||
|
"plural": false,
|
||||||
|
"selections": (v4/*: any*/),
|
||||||
|
"storageKey": "controls(first:100)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"filters": null,
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "Mitigation__controls",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "controls"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Mitigation",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "7429166728b5a233e15cb0f4cb33d15c",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {
|
||||||
|
"connection": [
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"cursor": null,
|
||||||
|
"direction": "forward",
|
||||||
|
"path": [
|
||||||
|
"mitigation",
|
||||||
|
"controls"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "MitigationViewLinkedControlsQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query MitigationViewLinkedControlsQuery(\n $mitigationId: ID!\n) {\n mitigation: node(id: $mitigationId) {\n __typename\n id\n ... on Mitigation {\n controls(first: 100) {\n edges {\n node {\n id\n referenceId\n name\n description\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "0748d068025e5f577ab56a8014ee38cc";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -30,8 +30,8 @@ type (
|
|||||||
Control struct {
|
Control struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
ReferenceID string `db:"reference_id"`
|
ReferenceID string `db:"reference_id"`
|
||||||
FrameworkID gid.GID `db:"framework_id"`
|
|
||||||
TenantID gid.TenantID `db:"tenant_id"`
|
TenantID gid.TenantID `db:"tenant_id"`
|
||||||
|
FrameworkID gid.GID `db:"framework_id"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Description string `db:"description"`
|
Description string `db:"description"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
@@ -56,6 +56,65 @@ func (c Control) CursorKey(orderBy ControlOrderField) page.CursorKey {
|
|||||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Controls) LoadByMitigationID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
mitigationID gid.GID,
|
||||||
|
cursor *page.Cursor[ControlOrderField],
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
WITH ctrl AS (
|
||||||
|
SELECT
|
||||||
|
c.id,
|
||||||
|
c.reference_id,
|
||||||
|
c.framework_id,
|
||||||
|
c.tenant_id,
|
||||||
|
c.name,
|
||||||
|
c.description,
|
||||||
|
c.created_at,
|
||||||
|
c.updated_at
|
||||||
|
FROM
|
||||||
|
controls c
|
||||||
|
INNER JOIN
|
||||||
|
controls_mitigations cm ON c.id = cm.control_id
|
||||||
|
WHERE
|
||||||
|
cm.mitigation_id = @mitigation_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
reference_id,
|
||||||
|
framework_id,
|
||||||
|
tenant_id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
ctrl
|
||||||
|
WHERE %s
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{"mitigation_id": mitigationID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query controls: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
controls, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Control])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect controls: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*c = controls
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Controls) LoadByFrameworkID(
|
func (c *Controls) LoadByFrameworkID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func (cm ControlMitigation) Insert(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
control_mitigations (
|
controls_mitigations (
|
||||||
control_id,
|
control_id,
|
||||||
mitigation_id,
|
mitigation_id,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
@@ -75,7 +75,7 @@ func (cm ControlMitigation) Delete(
|
|||||||
q := `
|
q := `
|
||||||
DELETE
|
DELETE
|
||||||
FROM
|
FROM
|
||||||
control_mitigations
|
controls_mitigations
|
||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND control_id = @control_id
|
AND control_id = @control_id
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
type (
|
type (
|
||||||
Mitigation struct {
|
Mitigation struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
|
TenantID gid.TenantID `db:"tenant_id"`
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
Category string `db:"category"`
|
Category string `db:"category"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
@@ -39,7 +40,6 @@ type (
|
|||||||
ContentRef string `db:"content_ref"`
|
ContentRef string `db:"content_ref"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
Version int `db:"version"`
|
|
||||||
Standards []string `db:"standards"`
|
Standards []string `db:"standards"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +55,123 @@ func (c Mitigation) CursorKey(orderBy MitigationOrderField) page.CursorKey {
|
|||||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Mitigations) LoadByControlID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
controlID gid.GID,
|
||||||
|
cursor *page.Cursor[MitigationOrderField],
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
WITH mtgtns AS (
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.tenant_id,
|
||||||
|
m.organization_id,
|
||||||
|
m.category,
|
||||||
|
m.name,
|
||||||
|
m.description,
|
||||||
|
m.state,
|
||||||
|
m.importance,
|
||||||
|
m.content_ref,
|
||||||
|
m.created_at,
|
||||||
|
m.updated_at,
|
||||||
|
m.standards
|
||||||
|
FROM
|
||||||
|
mitigations m
|
||||||
|
INNER JOIN
|
||||||
|
controls_mitigations cm ON m.id = cm.mitigation_id
|
||||||
|
WHERE
|
||||||
|
cm.control_id = @control_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
category,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
state,
|
||||||
|
importance,
|
||||||
|
content_ref,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
standards
|
||||||
|
FROM
|
||||||
|
mtgtns
|
||||||
|
WHERE %s
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"control_id": controlID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, cursor.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query mitigations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mitigations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Mitigation])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect mitigations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*c = mitigations
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Mitigations) LoadByOrganizationID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
organizationID gid.GID,
|
||||||
|
cursor *page.Cursor[MitigationOrderField],
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
category,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
state,
|
||||||
|
importance,
|
||||||
|
content_ref,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
standards
|
||||||
|
FROM
|
||||||
|
mitigations
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND organization_id = @organization_id
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, cursor.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query mitigations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mitigations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Mitigation])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect mitigations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*c = mitigations
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Mitigation) LoadByID(
|
func (c *Mitigation) LoadByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
@@ -64,6 +181,7 @@ func (c *Mitigation) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
tenant_id,
|
||||||
organization_id,
|
organization_id,
|
||||||
category,
|
category,
|
||||||
name,
|
name,
|
||||||
@@ -73,8 +191,7 @@ SELECT
|
|||||||
content_ref,
|
content_ref,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
standards,
|
standards
|
||||||
version
|
|
||||||
FROM
|
FROM
|
||||||
mitigations
|
mitigations
|
||||||
WHERE
|
WHERE
|
||||||
@@ -122,8 +239,7 @@ INSERT INTO
|
|||||||
content_ref,
|
content_ref,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
standards,
|
standards
|
||||||
version
|
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@tenant_id,
|
@tenant_id,
|
||||||
@@ -137,8 +253,7 @@ VALUES (
|
|||||||
@content_ref,
|
@content_ref,
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at,
|
@updated_at,
|
||||||
@standards,
|
@standards
|
||||||
@version
|
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -148,7 +263,6 @@ VALUES (
|
|||||||
"organization_id": c.OrganizationID,
|
"organization_id": c.OrganizationID,
|
||||||
"category": c.Category,
|
"category": c.Category,
|
||||||
"name": c.Name,
|
"name": c.Name,
|
||||||
"version": 0,
|
|
||||||
"description": c.Description,
|
"description": c.Description,
|
||||||
"content_ref": c.ContentRef,
|
"content_ref": c.ContentRef,
|
||||||
"created_at": c.CreatedAt,
|
"created_at": c.CreatedAt,
|
||||||
@@ -161,55 +275,6 @@ VALUES (
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Mitigations) LoadByOrganizationID(
|
|
||||||
ctx context.Context,
|
|
||||||
conn pg.Conn,
|
|
||||||
scope Scoper,
|
|
||||||
organizationID gid.GID,
|
|
||||||
cursor *page.Cursor[MitigationOrderField],
|
|
||||||
) error {
|
|
||||||
q := `
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
organization_id,
|
|
||||||
category,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
state,
|
|
||||||
importance,
|
|
||||||
content_ref,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
standards,
|
|
||||||
version
|
|
||||||
FROM
|
|
||||||
mitigations
|
|
||||||
WHERE
|
|
||||||
%s
|
|
||||||
AND organization_id = @organization_id
|
|
||||||
AND %s
|
|
||||||
`
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
|
||||||
maps.Copy(args, cursor.SQLArguments())
|
|
||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot query mitigations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mitigations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Mitigation])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot collect mitigations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*c = mitigations
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Mitigation) Update(
|
func (c *Mitigation) Update(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
|||||||
@@ -55,6 +55,67 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (s ControlService) ListForMitigationID(
|
||||||
|
ctx context.Context,
|
||||||
|
mitigationID gid.GID,
|
||||||
|
cursor *page.Cursor[coredata.ControlOrderField],
|
||||||
|
) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) {
|
||||||
|
var controls coredata.Controls
|
||||||
|
|
||||||
|
err := s.svc.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
return controls.LoadByMitigationID(ctx, conn, s.svc.scope, mitigationID, cursor)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot list controls: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.NewPage(controls, cursor), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ControlService) CreateMapping(
|
||||||
|
ctx context.Context,
|
||||||
|
controlID gid.GID,
|
||||||
|
mitigationID gid.GID,
|
||||||
|
) error {
|
||||||
|
controlMitigation := &coredata.ControlMitigation{
|
||||||
|
ControlID: controlID,
|
||||||
|
MitigationID: mitigationID,
|
||||||
|
TenantID: s.svc.scope.GetTenantID(),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.svc.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
return controlMitigation.Insert(ctx, conn, s.svc.scope)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ControlService) DeleteMapping(
|
||||||
|
ctx context.Context,
|
||||||
|
controlID gid.GID,
|
||||||
|
mitigationID gid.GID,
|
||||||
|
) error {
|
||||||
|
controlMitigation := &coredata.ControlMitigation{
|
||||||
|
ControlID: controlID,
|
||||||
|
MitigationID: mitigationID,
|
||||||
|
TenantID: s.svc.scope.GetTenantID(),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.svc.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
return controlMitigation.Delete(ctx, conn, s.svc.scope)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Create creates a new control
|
// Create creates a new control
|
||||||
func (s ControlService) Create(
|
func (s ControlService) Create(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|||||||
@@ -57,6 +57,27 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (s MitigationService) ListForControlID(
|
||||||
|
ctx context.Context,
|
||||||
|
controlID gid.GID,
|
||||||
|
cursor *page.Cursor[coredata.MitigationOrderField],
|
||||||
|
) (*page.Page[*coredata.Mitigation, coredata.MitigationOrderField], error) {
|
||||||
|
var mitigations coredata.Mitigations
|
||||||
|
|
||||||
|
err := s.svc.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
return mitigations.LoadByControlID(ctx, conn, s.svc.scope, controlID, cursor)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.NewPage(mitigations, cursor), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s MitigationService) Get(
|
func (s MitigationService) Get(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
mitigationID gid.GID,
|
mitigationID gid.GID,
|
||||||
|
|||||||
@@ -441,6 +441,15 @@ type Control implements Node {
|
|||||||
referenceId: String!
|
referenceId: String!
|
||||||
name: String!
|
name: String!
|
||||||
description: String!
|
description: String!
|
||||||
|
|
||||||
|
mitigations(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
orderBy: MitigationOrder
|
||||||
|
): MitigationConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
@@ -461,6 +470,22 @@ type Mitigation implements Node {
|
|||||||
orderBy: TaskOrder
|
orderBy: TaskOrder
|
||||||
): TaskConnection! @goField(forceResolver: true)
|
): TaskConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
|
risks(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
orderBy: RiskOrder
|
||||||
|
): RiskConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
|
controls(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
orderBy: ControlOrder
|
||||||
|
): ControlConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
@@ -517,6 +542,15 @@ type Risk implements Node {
|
|||||||
description: String!
|
description: String!
|
||||||
probability: Float!
|
probability: Float!
|
||||||
impact: Float!
|
impact: Float!
|
||||||
|
|
||||||
|
controls(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
orderBy: ControlOrder
|
||||||
|
): ControlConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
@@ -694,6 +728,14 @@ type Mutation {
|
|||||||
updateMitigation(input: UpdateMitigationInput!): UpdateMitigationPayload!
|
updateMitigation(input: UpdateMitigationInput!): UpdateMitigationPayload!
|
||||||
importMitigation(input: ImportMitigationInput!): ImportMitigationPayload!
|
importMitigation(input: ImportMitigationInput!): ImportMitigationPayload!
|
||||||
|
|
||||||
|
# Control mutations
|
||||||
|
createControlMapping(
|
||||||
|
input: CreateControlMappingInput!
|
||||||
|
): CreateControlMappingPayload!
|
||||||
|
deleteControlMapping(
|
||||||
|
input: DeleteControlMappingInput!
|
||||||
|
): DeleteControlMappingPayload!
|
||||||
|
|
||||||
# Task mutations
|
# Task mutations
|
||||||
createTask(input: CreateTaskInput!): CreateTaskPayload!
|
createTask(input: CreateTaskInput!): CreateTaskPayload!
|
||||||
updateTask(input: UpdateTaskInput!): UpdateTaskPayload!
|
updateTask(input: UpdateTaskInput!): UpdateTaskPayload!
|
||||||
@@ -853,6 +895,16 @@ input UnassignTaskInput {
|
|||||||
taskId: ID!
|
taskId: ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input CreateControlMappingInput {
|
||||||
|
controlId: ID!
|
||||||
|
mitigationId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
input DeleteControlMappingInput {
|
||||||
|
controlId: ID!
|
||||||
|
mitigationId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
input CreateRiskInput {
|
input CreateRiskInput {
|
||||||
organizationId: ID!
|
organizationId: ID!
|
||||||
name: String!
|
name: String!
|
||||||
@@ -1008,6 +1060,14 @@ type UnassignTaskPayload {
|
|||||||
task: Task!
|
task: Task!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateControlMappingPayload {
|
||||||
|
success: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteControlMappingPayload {
|
||||||
|
success: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
type CreateRiskPayload {
|
type CreateRiskPayload {
|
||||||
riskEdge: RiskEdge!
|
riskEdge: RiskEdge!
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -37,12 +37,13 @@ type ConfirmEmailPayload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Control struct {
|
type Control struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
ReferenceID string `json:"referenceId"`
|
ReferenceID string `json:"referenceId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
Mitigations *MitigationConnection `json:"mitigations"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Control) IsNode() {}
|
func (Control) IsNode() {}
|
||||||
@@ -58,6 +59,15 @@ type ControlEdge struct {
|
|||||||
Node *Control `json:"node"`
|
Node *Control `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateControlMappingInput struct {
|
||||||
|
ControlID gid.GID `json:"controlId"`
|
||||||
|
MitigationID gid.GID `json:"mitigationId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateControlMappingPayload struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
type CreateFrameworkInput struct {
|
type CreateFrameworkInput struct {
|
||||||
OrganizationID gid.GID `json:"organizationId"`
|
OrganizationID gid.GID `json:"organizationId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -154,6 +164,15 @@ type CreateVendorPayload struct {
|
|||||||
VendorEdge *VendorEdge `json:"vendorEdge"`
|
VendorEdge *VendorEdge `json:"vendorEdge"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteControlMappingInput struct {
|
||||||
|
ControlID gid.GID `json:"controlId"`
|
||||||
|
MitigationID gid.GID `json:"mitigationId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteControlMappingPayload struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteEvidenceInput struct {
|
type DeleteEvidenceInput struct {
|
||||||
EvidenceID gid.GID `json:"evidenceId"`
|
EvidenceID gid.GID `json:"evidenceId"`
|
||||||
}
|
}
|
||||||
@@ -303,6 +322,8 @@ type Mitigation struct {
|
|||||||
State coredata.MitigationState `json:"state"`
|
State coredata.MitigationState `json:"state"`
|
||||||
Importance coredata.MitigationImportance `json:"importance"`
|
Importance coredata.MitigationImportance `json:"importance"`
|
||||||
Tasks *TaskConnection `json:"tasks"`
|
Tasks *TaskConnection `json:"tasks"`
|
||||||
|
Risks *RiskConnection `json:"risks"`
|
||||||
|
Controls *ControlConnection `json:"controls"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
@@ -423,13 +444,14 @@ type RemoveUserPayload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Risk struct {
|
type Risk struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Probability float64 `json:"probability"`
|
Probability float64 `json:"probability"`
|
||||||
Impact float64 `json:"impact"`
|
Impact float64 `json:"impact"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
Controls *ControlConnection `json:"controls"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Risk) IsNode() {}
|
func (Risk) IsNode() {}
|
||||||
|
|||||||
@@ -19,6 +19,31 @@ import (
|
|||||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Mitigations is the resolver for the mitigations field.
|
||||||
|
func (r *controlResolver) Mitigations(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MitigationOrderBy) (*types.MitigationConnection, error) {
|
||||||
|
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
|
||||||
|
|
||||||
|
pageOrderBy := page.OrderBy[coredata.MitigationOrderField]{
|
||||||
|
Field: coredata.MitigationOrderFieldCreatedAt,
|
||||||
|
Direction: page.OrderDirectionDesc,
|
||||||
|
}
|
||||||
|
if orderBy != nil {
|
||||||
|
pageOrderBy = page.OrderBy[coredata.MitigationOrderField]{
|
||||||
|
Field: orderBy.Field,
|
||||||
|
Direction: orderBy.Direction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
|
page, err := svc.Mitigations.ListForControlID(ctx, obj.ID, cursor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot list mitigations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewMitigationConnection(page), nil
|
||||||
|
}
|
||||||
|
|
||||||
// FileURL is the resolver for the fileUrl field.
|
// FileURL is the resolver for the fileUrl field.
|
||||||
func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (*string, error) {
|
func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (*string, error) {
|
||||||
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
|
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
|
||||||
@@ -86,6 +111,36 @@ func (r *mitigationResolver) Tasks(ctx context.Context, obj *types.Mitigation, f
|
|||||||
return types.NewTaskConnection(page), nil
|
return types.NewTaskConnection(page), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Risks is the resolver for the risks field.
|
||||||
|
func (r *mitigationResolver) Risks(ctx context.Context, obj *types.Mitigation, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskOrderBy) (*types.RiskConnection, error) {
|
||||||
|
panic(fmt.Errorf("not implemented: Risks - risks"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controls is the resolver for the controls field.
|
||||||
|
func (r *mitigationResolver) Controls(ctx context.Context, obj *types.Mitigation, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error) {
|
||||||
|
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
|
||||||
|
|
||||||
|
pageOrderBy := page.OrderBy[coredata.ControlOrderField]{
|
||||||
|
Field: coredata.ControlOrderFieldCreatedAt,
|
||||||
|
Direction: page.OrderDirectionDesc,
|
||||||
|
}
|
||||||
|
if orderBy != nil {
|
||||||
|
pageOrderBy = page.OrderBy[coredata.ControlOrderField]{
|
||||||
|
Field: orderBy.Field,
|
||||||
|
Direction: orderBy.Direction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
|
page, err := svc.Controls.ListForMitigationID(ctx, obj.ID, cursor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot list mitigation controls: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewControlConnection(page), nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateOrganization is the resolver for the createOrganization field.
|
// CreateOrganization is the resolver for the createOrganization field.
|
||||||
func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) {
|
func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) {
|
||||||
svc := r.proboSvc.WithTenant(gid.NewTenantID())
|
svc := r.proboSvc.WithTenant(gid.NewTenantID())
|
||||||
@@ -446,6 +501,34 @@ func (r *mutationResolver) ImportMitigation(ctx context.Context, input types.Imp
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateControlMapping is the resolver for the createControlMapping field.
|
||||||
|
func (r *mutationResolver) CreateControlMapping(ctx context.Context, input types.CreateControlMappingInput) (*types.CreateControlMappingPayload, error) {
|
||||||
|
svc := r.GetTenantServiceIfAuthorized(ctx, input.MitigationID.TenantID())
|
||||||
|
|
||||||
|
err := svc.Controls.CreateMapping(ctx, input.ControlID, input.MitigationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create control mapping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.CreateControlMappingPayload{
|
||||||
|
Success: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteControlMapping is the resolver for the deleteControlMapping field.
|
||||||
|
func (r *mutationResolver) DeleteControlMapping(ctx context.Context, input types.DeleteControlMappingInput) (*types.DeleteControlMappingPayload, error) {
|
||||||
|
svc := r.GetTenantServiceIfAuthorized(ctx, input.MitigationID.TenantID())
|
||||||
|
|
||||||
|
err := svc.Controls.DeleteMapping(ctx, input.ControlID, input.MitigationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot delete control mapping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.DeleteControlMappingPayload{
|
||||||
|
Success: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateTask is the resolver for the createTask field.
|
// CreateTask is the resolver for the createTask field.
|
||||||
func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTaskInput) (*types.CreateTaskPayload, error) {
|
func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTaskInput) (*types.CreateTaskPayload, error) {
|
||||||
svc := r.GetTenantServiceIfAuthorized(ctx, input.MitigationID.TenantID())
|
svc := r.GetTenantServiceIfAuthorized(ctx, input.MitigationID.TenantID())
|
||||||
@@ -983,6 +1066,11 @@ func (r *queryResolver) Viewer(ctx context.Context) (*types.Viewer, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Controls is the resolver for the controls field.
|
||||||
|
func (r *riskResolver) Controls(ctx context.Context, obj *types.Risk, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error) {
|
||||||
|
panic(fmt.Errorf("not implemented: Controls - controls"))
|
||||||
|
}
|
||||||
|
|
||||||
// AssignedTo is the resolver for the assignedTo field.
|
// AssignedTo is the resolver for the assignedTo field.
|
||||||
func (r *taskResolver) AssignedTo(ctx context.Context, obj *types.Task) (*types.People, error) {
|
func (r *taskResolver) AssignedTo(ctx context.Context, obj *types.Task) (*types.People, error) {
|
||||||
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
|
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
|
||||||
@@ -1053,6 +1141,9 @@ func (r *viewerResolver) Organizations(ctx context.Context, obj *types.Viewer, f
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Control returns schema.ControlResolver implementation.
|
||||||
|
func (r *Resolver) Control() schema.ControlResolver { return &controlResolver{r} }
|
||||||
|
|
||||||
// Evidence returns schema.EvidenceResolver implementation.
|
// Evidence returns schema.EvidenceResolver implementation.
|
||||||
func (r *Resolver) Evidence() schema.EvidenceResolver { return &evidenceResolver{r} }
|
func (r *Resolver) Evidence() schema.EvidenceResolver { return &evidenceResolver{r} }
|
||||||
|
|
||||||
@@ -1074,12 +1165,16 @@ func (r *Resolver) Policy() schema.PolicyResolver { return &policyResolver{r} }
|
|||||||
// Query returns schema.QueryResolver implementation.
|
// Query returns schema.QueryResolver implementation.
|
||||||
func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} }
|
func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} }
|
||||||
|
|
||||||
|
// Risk returns schema.RiskResolver implementation.
|
||||||
|
func (r *Resolver) Risk() schema.RiskResolver { return &riskResolver{r} }
|
||||||
|
|
||||||
// Task returns schema.TaskResolver implementation.
|
// Task returns schema.TaskResolver implementation.
|
||||||
func (r *Resolver) Task() schema.TaskResolver { return &taskResolver{r} }
|
func (r *Resolver) Task() schema.TaskResolver { return &taskResolver{r} }
|
||||||
|
|
||||||
// Viewer returns schema.ViewerResolver implementation.
|
// Viewer returns schema.ViewerResolver implementation.
|
||||||
func (r *Resolver) Viewer() schema.ViewerResolver { return &viewerResolver{r} }
|
func (r *Resolver) Viewer() schema.ViewerResolver { return &viewerResolver{r} }
|
||||||
|
|
||||||
|
type controlResolver struct{ *Resolver }
|
||||||
type evidenceResolver struct{ *Resolver }
|
type evidenceResolver struct{ *Resolver }
|
||||||
type frameworkResolver struct{ *Resolver }
|
type frameworkResolver struct{ *Resolver }
|
||||||
type mitigationResolver struct{ *Resolver }
|
type mitigationResolver struct{ *Resolver }
|
||||||
@@ -1087,5 +1182,6 @@ type mutationResolver struct{ *Resolver }
|
|||||||
type organizationResolver struct{ *Resolver }
|
type organizationResolver struct{ *Resolver }
|
||||||
type policyResolver struct{ *Resolver }
|
type policyResolver struct{ *Resolver }
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
|
type riskResolver struct{ *Resolver }
|
||||||
type taskResolver struct{ *Resolver }
|
type taskResolver struct{ *Resolver }
|
||||||
type viewerResolver struct{ *Resolver }
|
type viewerResolver struct{ *Resolver }
|
||||||
|
|||||||
Reference in New Issue
Block a user