Add ability to link policies to controls

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-04-10 10:01:26 -07:00
parent c63e2756ba
commit 1196b791dd
8 changed files with 1310 additions and 6 deletions

View File

@@ -4,15 +4,17 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
## [0.4.2] - 2025-04-09
### Added
- Add vendor compliance reports UI
- Controls can now be linked to policies, enabling better organization of compliance documentation and clearer traceability between policies and security controls
- New UI for viewing and managing policies related to a specific control
## [0.4.2] - 2025-04-09
### Changed
- Simplified policy data model by removing version field and optimistic concurrency
- Simplified policy data model by removing version field and optimistic concurrency
- Refactored policy update flow to load-modify-save pattern
### Fixed

View File

@@ -34,6 +34,14 @@ import {
ControlOrganizationMitigationsQuery,
} from "./__generated__/ControlOrganizationMitigationsQuery.graphql";
import { ControlFragment_Control$key } from "./__generated__/ControlFragment_Control.graphql";
import {
ControlLinkedPoliciesQuery$data,
ControlLinkedPoliciesQuery,
} from "./__generated__/ControlLinkedPoliciesQuery.graphql";
import {
ControlOrganizationPoliciesQuery$data,
ControlOrganizationPoliciesQuery,
} from "./__generated__/ControlOrganizationPoliciesQuery.graphql";
const controlFragment = graphql`
fragment ControlFragment_Control on Control {
@@ -90,6 +98,50 @@ const organizationMitigationsQuery = graphql`
}
`;
// Query to fetch linked policies
const linkedPoliciesQuery = graphql`
query ControlLinkedPoliciesQuery($controlId: ID!) {
control: node(id: $controlId) {
id
... on Control {
policies(first: 100) @connection(key: "Control__policies") {
edges {
node {
id
name
content
status
reviewDate
}
}
}
}
}
}
`;
// Query to fetch all policies for the organization
const organizationPoliciesQuery = graphql`
query ControlOrganizationPoliciesQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
id
... on Organization {
policies(first: 100) @connection(key: "Organization__policies") {
edges {
node {
id
name
content
status
reviewDate
}
}
}
}
}
}
`;
// Mutation to create a mapping between a control and a mitigation
const createMitigationMappingMutation = graphql`
mutation ControlCreateMitigationMappingMutation(
@@ -112,6 +164,28 @@ const deleteMitigationMappingMutation = graphql`
}
`;
// Mutation to create a mapping between a control and a policy
const createPolicyMappingMutation = graphql`
mutation ControlCreatePolicyMappingMutation(
$input: CreateControlPolicyMappingInput!
) {
createControlPolicyMapping(input: $input) {
success
}
}
`;
// Mutation to delete a mapping between a control and a policy
const deletePolicyMappingMutation = graphql`
mutation ControlDeletePolicyMappingMutation(
$input: DeleteControlPolicyMappingInput!
) {
deleteControlPolicyMapping(input: $input) {
success
}
}
`;
export function Control({
controlKey,
}: {
@@ -138,6 +212,18 @@ export function Control({
const [isUnlinkingMitigation, setIsUnlinkingMitigation] = useState(false);
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
// Policy state
const [isPolicyMappingDialogOpen, setIsPolicyMappingDialogOpen] =
useState(false);
const [linkedPoliciesData, setLinkedPoliciesData] =
useState<ControlLinkedPoliciesQuery$data | null>(null);
const [organizationPoliciesData, setOrganizationPoliciesData] =
useState<ControlOrganizationPoliciesQuery$data | null>(null);
const [policySearchQuery, setPolicySearchQuery] = useState("");
const [isLoadingPolicies, setIsLoadingPolicies] = useState(false);
const [isLinkingPolicy, setIsLinkingPolicy] = useState(false);
const [isUnlinkingPolicy, setIsUnlinkingPolicy] = useState(false);
// Create mutation hooks
const [commitCreateMitigationMapping] = useMutation(
createMitigationMappingMutation
@@ -145,6 +231,8 @@ export function Control({
const [commitDeleteMitigationMapping] = useMutation(
deleteMitigationMappingMutation
);
const [commitCreatePolicyMapping] = useMutation(createPolicyMappingMutation);
const [commitDeletePolicyMapping] = useMutation(deletePolicyMappingMutation);
// Load initial linked mitigations data
useEffect(() => {
@@ -415,6 +503,243 @@ export function Control({
setIsMitigationMappingDialogOpen(true);
}, [loadMitigationsData]);
// Load initial linked policies data
useEffect(() => {
if (control.id) {
setIsLoadingPolicies(true);
fetchQuery<ControlLinkedPoliciesQuery>(environment, linkedPoliciesQuery, {
controlId: control.id,
}).subscribe({
next: (data) => {
setLinkedPoliciesData(data);
setIsLoadingPolicies(false);
},
error: (error: Error) => {
console.error("Error loading initial policies:", error);
setIsLoadingPolicies(false);
},
});
}
}, [control.id, environment]);
// Load policies data
const loadPoliciesData = useCallback(() => {
if (!organizationId || !control.id) return;
setIsLoadingPolicies(true);
// Fetch all policies for the organization
fetchQuery<ControlOrganizationPoliciesQuery>(
environment,
organizationPoliciesQuery,
{
organizationId,
}
).subscribe({
next: (data) => {
setOrganizationPoliciesData(data);
},
complete: () => {
// Fetch linked policies for this control
fetchQuery<ControlLinkedPoliciesQuery>(
environment,
linkedPoliciesQuery,
{
controlId: control.id,
}
).subscribe({
next: (data) => {
setLinkedPoliciesData(data);
setIsLoadingPolicies(false);
},
error: (error: Error) => {
console.error("Error fetching linked policies:", error);
setIsLoadingPolicies(false);
toast({
title: "Error",
description: "Failed to load linked policies.",
variant: "destructive",
});
},
});
},
error: (error: Error) => {
console.error("Error fetching organization policies:", error);
setIsLoadingPolicies(false);
toast({
title: "Error",
description: "Failed to load policies.",
variant: "destructive",
});
},
});
}, [control.id, environment, organizationId, toast]);
// Policy helper functions
const getPolicies = useCallback(() => {
if (!organizationPoliciesData?.organization?.policies?.edges) return [];
return organizationPoliciesData.organization.policies.edges.map(
(edge) => edge.node
);
}, [organizationPoliciesData]);
const getLinkedPolicies = useCallback(() => {
if (!linkedPoliciesData?.control?.policies?.edges) return [];
return linkedPoliciesData.control.policies.edges.map((edge) => edge.node);
}, [linkedPoliciesData]);
const isPolicyLinked = useCallback(
(policyId: string) => {
const linkedPolicies = getLinkedPolicies();
return linkedPolicies.some((policy) => policy.id === policyId);
},
[getLinkedPolicies]
);
const filteredPolicies = useCallback(() => {
const policies = getPolicies();
if (!policySearchQuery) return policies;
return policies.filter((policy) => {
return (
!policySearchQuery ||
policy.name.toLowerCase().includes(policySearchQuery.toLowerCase()) ||
(policy.content &&
policy.content
.toLowerCase()
.includes(policySearchQuery.toLowerCase()))
);
});
}, [getPolicies, policySearchQuery]);
// Policy link/unlink handlers
const handleLinkPolicy = useCallback(
(policyId: string) => {
if (!control.id) return;
setIsLinkingPolicy(true);
commitCreatePolicyMapping({
variables: {
input: {
controlId: control.id,
policyId: policyId,
},
},
onCompleted: (_, errors) => {
setIsLinkingPolicy(false);
if (errors) {
console.error("Error linking policy:", errors);
toast({
title: "Error",
description: "Failed to link policy. Please try again.",
variant: "destructive",
});
return;
}
// Refresh linked policies data
fetchQuery<ControlLinkedPoliciesQuery>(
environment,
linkedPoliciesQuery,
{
controlId: control.id,
}
).subscribe({
next: (data) => {
setLinkedPoliciesData(data);
},
error: (error: Error) => {
console.error("Error refreshing linked policies:", error);
},
});
toast({
title: "Success",
description: "Policy successfully linked to control.",
});
},
onError: (error) => {
setIsLinkingPolicy(false);
console.error("Error linking policy:", error);
toast({
title: "Error",
description: "Failed to link policy. Please try again.",
variant: "destructive",
});
},
});
},
[commitCreatePolicyMapping, control.id, environment, toast]
);
const handleUnlinkPolicy = useCallback(
(policyId: string) => {
if (!control.id) return;
setIsUnlinkingPolicy(true);
commitDeletePolicyMapping({
variables: {
input: {
controlId: control.id,
policyId: policyId,
},
},
onCompleted: (_, errors) => {
setIsUnlinkingPolicy(false);
if (errors) {
console.error("Error unlinking policy:", errors);
toast({
title: "Error",
description: "Failed to unlink policy. Please try again.",
variant: "destructive",
});
return;
}
// Refresh linked policies data
fetchQuery<ControlLinkedPoliciesQuery>(
environment,
linkedPoliciesQuery,
{
controlId: control.id,
}
).subscribe({
next: (data) => {
setLinkedPoliciesData(data);
},
error: (error: Error) => {
console.error("Error refreshing linked policies:", error);
},
});
toast({
title: "Success",
description: "Policy successfully unlinked from control.",
});
},
onError: (error) => {
setIsUnlinkingPolicy(false);
console.error("Error unlinking policy:", error);
toast({
title: "Error",
description: "Failed to unlink policy. Please try again.",
variant: "destructive",
});
},
});
},
[commitDeletePolicyMapping, control.id, environment, toast]
);
const handleOpenPolicyMappingDialog = useCallback(() => {
loadPoliciesData();
setIsPolicyMappingDialogOpen(true);
}, [loadPoliciesData]);
// UI helper functions
const formatImportance = (importance: string | undefined): string => {
if (!importance) return "Unknown";
@@ -773,6 +1098,256 @@ export function Control({
)}
</div>
</div>
{/* Policies Section */}
<div className="mt-8">
{/* Policy Mapping Dialog */}
<Dialog
open={isPolicyMappingDialogOpen}
onOpenChange={setIsPolicyMappingDialogOpen}
>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Link Policies to Control</DialogTitle>
<DialogDescription>
Search and select policies to link to this control. This helps
track which policies 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-tertiary" />
<Input
placeholder="Search policies by name or content..."
value={policySearchQuery}
onChange={(e) => setPolicySearchQuery(e.target.value)}
className="w-full pl-10"
/>
</div>
</div>
</div>
<div className="flex-1 overflow-hidden">
{isLoadingPolicies ? (
<div className="flex items-center justify-center h-full">
<Loader2 className="w-8 h-8 animate-spin text-info" />
<span className="ml-2">Loading policies...</span>
</div>
) : (
<div className="max-h-[50vh] overflow-y-auto pr-2">
{filteredPolicies().length === 0 ? (
<div className="text-center py-8 text-secondary">
No policies found. Try adjusting your search.
</div>
) : (
<table className="w-full bg-level-1">
<thead className="sticky top-0 bg-white">
<tr className="border-b text-left text-sm text-secondary bg-invert-bg">
<th className="py-3 px-4 font-medium">Name</th>
<th className="py-3 px-4 font-medium">Status</th>
<th className="py-3 px-4 font-medium">
Review Date
</th>
<th className="py-3 px-4 font-medium text-right">
Actions
</th>
</tr>
</thead>
<tbody>
{filteredPolicies().map((policy) => {
const isLinked = isPolicyLinked(policy.id);
return (
<tr
key={policy.id}
className="border-b hover:bg-invert-bg"
>
<td className="py-3 px-4">
<div className="font-medium">
{policy.name}
</div>
{policy.content && (
<div className="text-xs text-secondary line-clamp-1 mt-0.5">
{policy.content}
</div>
)}
</td>
<td className="py-3 px-4">
<div
className={`px-2 py-0.5 rounded-full text-xs ${
policy.status === "ACTIVE"
? "bg-success-bg text-success"
: "bg-secondary-bg text-secondary"
} inline-block`}
>
{policy.status}
</div>
</td>
<td className="py-3 px-4">
{policy.reviewDate
? new Date(
policy.reviewDate
).toLocaleDateString()
: "Not set"}
</td>
<td className="py-3 px-4 text-right whitespace-nowrap">
{isLinked ? (
<Button
variant="outline"
size="sm"
onClick={() =>
handleUnlinkPolicy(policy.id)
}
disabled={isUnlinkingPolicy}
className="text-xs h-7 text-danger border-danger-b hover:bg-h-danger-bg"
>
{isUnlinkingPolicy ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<X className="w-4 h-4" />
)}
<span className="ml-1">Unlink</span>
</Button>
) : (
<Button
variant="secondary"
size="sm"
onClick={() =>
handleLinkPolicy(policy.id)
}
disabled={isLinkingPolicy}
className="text-xs h-7 text-info border-info-b hover:bg-h-info-bg"
>
{isLinkingPolicy ? (
<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={() => setIsPolicyMappingDialogOpen(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Linked Policies List */}
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-medium text-secondary">Policies</h3>
<Button
variant="outline"
size="sm"
className="flex items-center gap-1"
onClick={handleOpenPolicyMappingDialog}
>
<LinkIcon className="w-4 h-4" />
<span>Link Policies</span>
</Button>
</div>
{isLoadingPolicies ? (
<div className="flex items-center justify-center h-24">
<Loader2 className="w-6 h-6 animate-spin text-info" />
<span className="ml-2">Loading policies...</span>
</div>
) : linkedPoliciesData?.control?.policies?.edges &&
linkedPoliciesData.control.policies.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-secondary bg-invert-bg">
<th className="py-3 px-4 font-medium">Name</th>
<th className="py-3 px-4 font-medium">Status</th>
<th className="py-3 px-4 font-medium">Review Date</th>
<th className="py-3 px-4 font-medium text-right">
Actions
</th>
</tr>
</thead>
<tbody>
{getLinkedPolicies().map((policy) => (
<tr
key={policy.id}
className="border-b hover:bg-invert-bg"
>
<td className="py-3 px-4">
<div className="font-medium">{policy.name}</div>
{policy.content && (
<div className="text-xs text-secondary line-clamp-1 mt-0.5">
{policy.content}
</div>
)}
</td>
<td className="py-3 px-4">
<div
className={`px-2 py-0.5 rounded-full text-xs ${
policy.status === "ACTIVE"
? "bg-success-bg text-success"
: "bg-secondary-bg text-secondary"
} inline-block`}
>
{policy.status}
</div>
</td>
<td className="py-3 px-4">
{policy.reviewDate
? new Date(policy.reviewDate).toLocaleDateString()
: "Not set"}
</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}/policies/${policy.id}`}
>
View
</Link>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleUnlinkPolicy(policy.id)}
disabled={isUnlinkingPolicy}
className="text-xs h-7 text-danger border-danger-b hover:bg-h-danger-bg"
>
Unlink
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center py-8 text-secondary border rounded-md">
No policies linked to this control yet. Click &quot;Link
Policies&quot; to connect some.
</div>
)}
</div>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,93 @@
/**
* @generated SignedSource<<d614f25231ff53581f96021c5b52628a>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type CreateControlPolicyMappingInput = {
controlId: string;
policyId: string;
};
export type ControlCreatePolicyMappingMutation$variables = {
input: CreateControlPolicyMappingInput;
};
export type ControlCreatePolicyMappingMutation$data = {
readonly createControlPolicyMapping: {
readonly success: boolean;
};
};
export type ControlCreatePolicyMappingMutation = {
response: ControlCreatePolicyMappingMutation$data;
variables: ControlCreatePolicyMappingMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "CreateControlPolicyMappingPayload",
"kind": "LinkedField",
"name": "createControlPolicyMapping",
"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": "ControlCreatePolicyMappingMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ControlCreatePolicyMappingMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "bde220ef49867cec6b77bffc06878258",
"id": null,
"metadata": {},
"name": "ControlCreatePolicyMappingMutation",
"operationKind": "mutation",
"text": "mutation ControlCreatePolicyMappingMutation(\n $input: CreateControlPolicyMappingInput!\n) {\n createControlPolicyMapping(input: $input) {\n success\n }\n}\n"
}
};
})();
(node as any).hash = "8e6ef41146efb59340331e16ce0493d6";
export default node;

View File

@@ -0,0 +1,93 @@
/**
* @generated SignedSource<<5988b98a6e2ae42a565e51fc13fa5c91>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeleteControlPolicyMappingInput = {
controlId: string;
policyId: string;
};
export type ControlDeletePolicyMappingMutation$variables = {
input: DeleteControlPolicyMappingInput;
};
export type ControlDeletePolicyMappingMutation$data = {
readonly deleteControlPolicyMapping: {
readonly success: boolean;
};
};
export type ControlDeletePolicyMappingMutation = {
response: ControlDeletePolicyMappingMutation$data;
variables: ControlDeletePolicyMappingMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "DeleteControlPolicyMappingPayload",
"kind": "LinkedField",
"name": "deleteControlPolicyMapping",
"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": "ControlDeletePolicyMappingMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ControlDeletePolicyMappingMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "3b84cd899eee67303695eae81d1ba453",
"id": null,
"metadata": {},
"name": "ControlDeletePolicyMappingMutation",
"operationKind": "mutation",
"text": "mutation ControlDeletePolicyMappingMutation(\n $input: DeleteControlPolicyMappingInput!\n) {\n deleteControlPolicyMapping(input: $input) {\n success\n }\n}\n"
}
};
})();
(node as any).hash = "e706f52dac09ef3fcadeb588ccd0274d";
export default node;

View File

@@ -0,0 +1,271 @@
/**
* @generated SignedSource<<9bddb18bf52f14881a85995793bdc1ca>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type ControlLinkedPoliciesQuery$variables = {
controlId: string;
};
export type ControlLinkedPoliciesQuery$data = {
readonly control: {
readonly id: string;
readonly policies?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly content: string;
readonly id: string;
readonly name: string;
readonly reviewDate: string | null | undefined;
readonly status: PolicyStatus;
};
}>;
};
};
};
export type ControlLinkedPoliciesQuery = {
response: ControlLinkedPoliciesQuery$data;
variables: ControlLinkedPoliciesQuery$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": "PolicyEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Policy",
"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": "content",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "reviewDate",
"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": "ControlLinkedPoliciesQuery",
"selections": [
{
"alias": "control",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": "policies",
"args": null,
"concreteType": "PolicyConnection",
"kind": "LinkedField",
"name": "__Control__policies_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": "ControlLinkedPoliciesQuery",
"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": "PolicyConnection",
"kind": "LinkedField",
"name": "policies",
"plural": false,
"selections": (v4/*: any*/),
"storageKey": "policies(first:100)"
},
{
"alias": null,
"args": (v5/*: any*/),
"filters": null,
"handle": "connection",
"key": "Control__policies",
"kind": "LinkedHandle",
"name": "policies"
}
],
"type": "Control",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "67f9bf1d5f255f5406e932abde540e08",
"id": null,
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"control",
"policies"
]
}
]
},
"name": "ControlLinkedPoliciesQuery",
"operationKind": "query",
"text": "query ControlLinkedPoliciesQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n policies(first: 100) {\n edges {\n node {\n id\n name\n content\n status\n reviewDate\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "3904c2344754177177fa7c16129dc894";
export default node;

View File

@@ -0,0 +1,271 @@
/**
* @generated SignedSource<<cfe85b9fdabaf24f9a7f575eca8a6417>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type ControlOrganizationPoliciesQuery$variables = {
organizationId: string;
};
export type ControlOrganizationPoliciesQuery$data = {
readonly organization: {
readonly id: string;
readonly policies?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly content: string;
readonly id: string;
readonly name: string;
readonly reviewDate: string | null | undefined;
readonly status: PolicyStatus;
};
}>;
};
};
};
export type ControlOrganizationPoliciesQuery = {
response: ControlOrganizationPoliciesQuery$data;
variables: ControlOrganizationPoliciesQuery$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": "PolicyEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Policy",
"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": "content",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "reviewDate",
"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": "ControlOrganizationPoliciesQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": "policies",
"args": null,
"concreteType": "PolicyConnection",
"kind": "LinkedField",
"name": "__Organization__policies_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": "ControlOrganizationPoliciesQuery",
"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": "PolicyConnection",
"kind": "LinkedField",
"name": "policies",
"plural": false,
"selections": (v4/*: any*/),
"storageKey": "policies(first:100)"
},
{
"alias": null,
"args": (v5/*: any*/),
"filters": null,
"handle": "connection",
"key": "Organization__policies",
"kind": "LinkedHandle",
"name": "policies"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "3a3c91f3e18e8a4b66011aa583e262e4",
"id": null,
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"organization",
"policies"
]
}
]
},
"name": "ControlOrganizationPoliciesQuery",
"operationKind": "query",
"text": "query ControlOrganizationPoliciesQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n policies(first: 100) {\n edges {\n node {\n id\n name\n content\n status\n reviewDate\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "f4699d729e6df6192efd201047689204";
export default node;

View File

@@ -43,7 +43,7 @@ func (cp ControlPolicy) Insert(
) error {
q := `
INSERT INTO
controls_mitigations (
controls_policies (
control_id,
policy_id,
tenant_id,
@@ -75,7 +75,7 @@ func (cp ControlPolicy) Delete(
q := `
DELETE
FROM
controls_mitigations
controls_policies
WHERE
%s
AND control_id = @control_id

View File

@@ -266,7 +266,6 @@ WITH plcs AS (
)
SELECT
id,
tenant_id,
organization_id,
owner_id,
name,