From 8d57c4596186edc95bf39fc6bed2777e2902c9a1 Mon Sep 17 00:00:00 2001 From: ianedwards Date: Fri, 2 Feb 2024 17:41:28 -0500 Subject: [PATCH] show hipaa check status on the compliance dashboard (#4227) --- .../handlers/cluster/compliance_checks.go | 18 ++++++- dashboard/package-lock.json | 14 ++--- dashboard/package.json | 2 +- .../compliance-dashboard/ActionBanner.tsx | 40 ++++++++++----- .../ComplianceContext.tsx | 39 +++++++++++--- .../ComplianceDashboard.tsx | 29 +---------- .../compliance-dashboard/ConfigSelectors.tsx | 20 +++++--- .../compliance-dashboard/ProfileHeader.tsx | 51 +++++++++++++++++++ .../compliance-dashboard/SOC2CostConsent.tsx | 32 ++++++++---- .../compliance-dashboard/VendorChecksList.tsx | 16 +++++- dashboard/src/shared/api.tsx | 2 +- go.mod | 2 +- go.sum | 10 +--- internal/compliance/convert.go | 10 ++++ 14 files changed, 200 insertions(+), 85 deletions(-) create mode 100644 dashboard/src/main/home/compliance-dashboard/ProfileHeader.tsx diff --git a/api/server/handlers/cluster/compliance_checks.go b/api/server/handlers/cluster/compliance_checks.go index c6052572e7..3a0629fb24 100644 --- a/api/server/handlers/cluster/compliance_checks.go +++ b/api/server/handlers/cluster/compliance_checks.go @@ -33,7 +33,8 @@ func NewListComplianceChecksHandler( // ListComplianceChecksRequest is the expected format for a request to /compliance/checks type ListComplianceChecksRequest struct { - Vendor compliance.Vendor `schema:"vendor"` + Vendor compliance.Vendor `schema:"vendor"` + Profile compliance.Profile `schema:"profile"` } // ListComplianceChecksResponse is the expected format for a response from /compliance/checks @@ -69,10 +70,25 @@ func (c *ListComplianceChecksHandler) ServeHTTP(w http.ResponseWriter, r *http.R } } + var profile porterv1.EnumComplianceProfile + if request.Profile != "" { + switch request.Profile { + case compliance.Profile_SOC2: + profile = porterv1.EnumComplianceProfile_ENUM_COMPLIANCE_PROFILE_SOC2 + case compliance.Profile_HIPAA: + profile = porterv1.EnumComplianceProfile_ENUM_COMPLIANCE_PROFILE_HIPAA + default: + err := telemetry.Error(ctx, span, nil, "invalid profile") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + } + req := connect.NewRequest(&porterv1.ContractComplianceChecksRequest{ ProjectId: int64(project.ID), ClusterId: int64(cluster.ID), Vendor: vendor, + Profile: profile, }) ccpResp, err := c.Config().ClusterControlPlaneClient.ContractComplianceChecks(ctx, req) diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 442129018f..44775bb697 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -95,7 +95,7 @@ "@babel/preset-typescript": "^7.15.0", "@ianvs/prettier-plugin-sort-imports": "^4.1.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", - "@porter-dev/api-contracts": "^0.2.84", + "@porter-dev/api-contracts": "^0.2.97", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", @@ -2754,9 +2754,9 @@ } }, "node_modules/@porter-dev/api-contracts": { - "version": "0.2.84", - "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.84.tgz", - "integrity": "sha512-KNwaVBLkW95LQSxkxs+mwtJp8MIArS0X9ZODpVPjW/ZPjQ+PvCWoOsyNShoyOp2YrzaByKcXBDeKCsT2ZCgdCw==", + "version": "0.2.97", + "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.97.tgz", + "integrity": "sha512-LceOZWw1zjWH3E/i9GO6cRfc9KSP68ofR0YTxmIruAg3V2xCjvtW762r9NrXzJE4TkJryodCE+e7ZE2J0LVJPg==", "dev": true, "dependencies": { "@bufbuild/protobuf": "^1.1.0" @@ -20056,9 +20056,9 @@ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@porter-dev/api-contracts": { - "version": "0.2.84", - "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.84.tgz", - "integrity": "sha512-KNwaVBLkW95LQSxkxs+mwtJp8MIArS0X9ZODpVPjW/ZPjQ+PvCWoOsyNShoyOp2YrzaByKcXBDeKCsT2ZCgdCw==", + "version": "0.2.97", + "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.97.tgz", + "integrity": "sha512-LceOZWw1zjWH3E/i9GO6cRfc9KSP68ofR0YTxmIruAg3V2xCjvtW762r9NrXzJE4TkJryodCE+e7ZE2J0LVJPg==", "dev": true, "requires": { "@bufbuild/protobuf": "^1.1.0" diff --git a/dashboard/package.json b/dashboard/package.json index dba4c2b540..e34dd9b439 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -102,7 +102,7 @@ "@babel/preset-typescript": "^7.15.0", "@ianvs/prettier-plugin-sort-imports": "^4.1.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", - "@porter-dev/api-contracts": "^0.2.84", + "@porter-dev/api-contracts": "^0.2.97", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", diff --git a/dashboard/src/main/home/compliance-dashboard/ActionBanner.tsx b/dashboard/src/main/home/compliance-dashboard/ActionBanner.tsx index 1152d9ad74..09f3c8cf62 100644 --- a/dashboard/src/main/home/compliance-dashboard/ActionBanner.tsx +++ b/dashboard/src/main/home/compliance-dashboard/ActionBanner.tsx @@ -1,5 +1,6 @@ import React, { useMemo, type Dispatch, type SetStateAction } from "react"; import { useHistory } from "react-router"; +import { match } from "ts-pattern"; import Banner from "components/porter/Banner"; import Image from "components/porter/Image"; @@ -20,10 +21,11 @@ export const ActionBanner: React.FC = ({ }) => { const history = useHistory(); const { + profile, updateInProgress, latestContractDB, latestContractProto, - updateContractWithSOC2, + updateContractWithProfile, } = useCompliance(); const provisioningStatus = useMemo(() => { @@ -60,21 +62,30 @@ export const ActionBanner: React.FC = ({ return provisioningStatus.state === "pending" || updateInProgress; }, [provisioningStatus.state, updateInProgress]); + const complianceEnabled = match(profile) + .with("soc2", () => latestContractProto?.complianceProfiles?.soc2) + .with("hipaa", () => latestContractProto?.complianceProfiles?.hipaa) + .exhaustive(); + // check if compliance has not been enable or if not all checks have passed const actionRequredWithoutProvisioningError = useMemo(() => { return ( - provisioningStatus.state === "compliance_error" || - !latestContractProto?.cluster?.isSoc2Compliant + provisioningStatus.state === "compliance_error" || !complianceEnabled ); - }, [provisioningStatus.state, latestContractProto?.toJsonString()]); + }, [ + provisioningStatus.state, + latestContractProto?.toJsonString(), + complianceEnabled, + ]); // check if provisioning error is due to compliance update const provisioningErrorWithComplianceEnabled = useMemo(() => { - return ( - provisioningStatus.state === "failed" && - latestContractProto?.cluster?.isSoc2Compliant - ); - }, [provisioningStatus.state, latestContractProto?.toJsonString()]); + return provisioningStatus.state === "failed" && complianceEnabled; + }, [ + provisioningStatus.state, + latestContractProto?.toJsonString(), + complianceEnabled, + ]); if (isInfraPending) { return ( @@ -83,8 +94,8 @@ export const ActionBanner: React.FC = ({ } > - SOC 2 infrastructure controls are being enabled. Note: This may take up - to 30 minutes. + Infrastructure controls are being enabled. Note: This may take up to 30 + minutes. ); } @@ -101,7 +112,7 @@ export const ActionBanner: React.FC = ({ cursor: "pointer", }} onClick={() => { - void updateContractWithSOC2(); + void updateContractWithProfile(); }} > Re-run infrastructure controls @@ -116,7 +127,8 @@ export const ActionBanner: React.FC = ({ setShowCostConsentModal(true); }} > - Enable SOC 2 infrastructure controls + Enable {profile === "soc2" ? "SOC2" : "HIPAA"} infrastructure + controls )} @@ -146,7 +158,7 @@ export const ActionBanner: React.FC = ({ cursor: "pointer", }} onClick={() => { - void updateContractWithSOC2(); + void updateContractWithProfile(); }} > diff --git a/dashboard/src/main/home/compliance-dashboard/ComplianceContext.tsx b/dashboard/src/main/home/compliance-dashboard/ComplianceContext.tsx index f4210775cc..7ece773b2b 100644 --- a/dashboard/src/main/home/compliance-dashboard/ComplianceContext.tsx +++ b/dashboard/src/main/home/compliance-dashboard/ComplianceContext.tsx @@ -1,5 +1,17 @@ -import React, { createContext, useContext, useMemo, useState } from "react"; -import { Contract, EKS, EKSLogging } from "@porter-dev/api-contracts"; +import React, { + createContext, + useContext, + useMemo, + useState, + type Dispatch, + type SetStateAction, +} from "react"; +import { + ComplianceProfile, + Contract, + EKS, + EKSLogging, +} from "@porter-dev/api-contracts"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { match } from "ts-pattern"; import { z } from "zod"; @@ -15,6 +27,8 @@ import { type VendorCheck, } from "./types"; +type ComplianceProfileType = "soc2" | "hipaa"; + type ProjectComplianceContextType = { projectId: number; clusterId: number; @@ -25,7 +39,9 @@ type ProjectComplianceContextType = { checksLoading: boolean; contractLoading: boolean; updateInProgress: boolean; - updateContractWithSOC2: () => Promise; + profile: ComplianceProfileType; + setProfile: Dispatch>; + updateContractWithProfile: () => Promise; }; const ProjectComplianceContext = @@ -52,6 +68,7 @@ export const ProjectComplianceProvider: React.FC< > = ({ projectId, clusterId, children }) => { const queryClient = useQueryClient(); const [updateInProgress, setUpdateInProgress] = useState(false); + const [profile, setProfile] = useState("soc2"); const { data: baseContract, isLoading: contractLoading } = useQuery( [projectId, clusterId, "getContracts"], @@ -80,13 +97,14 @@ export const ProjectComplianceProvider: React.FC< projectId, clusterId, condition: baseContract?.condition ?? "", + profile, name: "getComplianceChecks", }, ], async () => { const res = await api.getComplianceChecks( "", - { vendor: "vanta" }, + { vendor: "vanta", profile }, { projectId, clusterId } ); @@ -114,7 +132,7 @@ export const ProjectComplianceProvider: React.FC< }); }, [baseContract?.base64_contract]); - const updateContractWithSOC2 = async (): Promise => { + const updateContractWithProfile = async (): Promise => { try { setUpdateInProgress(true); @@ -141,6 +159,12 @@ export const ProjectComplianceProvider: React.FC< })) .otherwise((kind) => kind); + const complianceProfiles = new ComplianceProfile({ + ...latestContract.complianceProfiles, + ...(profile === "soc2" && { soc2: true }), + ...(profile === "hipaa" && { hipaa: true }), + }); + const updatedContract = new Contract({ ...latestContract, cluster: { @@ -148,6 +172,7 @@ export const ProjectComplianceProvider: React.FC< kindValues: updatedKindValues, isSoc2Compliant: true, }, + complianceProfiles, }); await api.createContract("", updatedContract, { @@ -176,7 +201,9 @@ export const ProjectComplianceProvider: React.FC< checksLoading, contractLoading, updateInProgress, - updateContractWithSOC2, + profile, + setProfile, + updateContractWithProfile, }} > {children} diff --git a/dashboard/src/main/home/compliance-dashboard/ComplianceDashboard.tsx b/dashboard/src/main/home/compliance-dashboard/ComplianceDashboard.tsx index 996f673138..dcd6a7cb50 100644 --- a/dashboard/src/main/home/compliance-dashboard/ComplianceDashboard.tsx +++ b/dashboard/src/main/home/compliance-dashboard/ComplianceDashboard.tsx @@ -2,20 +2,16 @@ import React, { useContext, useState } from "react"; import styled from "styled-components"; import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder"; -import Container from "components/porter/Container"; -import Image from "components/porter/Image"; import Spacer from "components/porter/Spacer"; -import Text from "components/porter/Text"; import DashboardHeader from "main/home/cluster-dashboard/DashboardHeader"; import { Context } from "shared/Context"; import compliance from "assets/compliance.svg"; -import linkExternal from "assets/link-external.svg"; -import vanta from "assets/vanta.svg"; import { ActionBanner } from "./ActionBanner"; import { ProjectComplianceProvider } from "./ComplianceContext"; import { ConfigSelectors } from "./ConfigSelectors"; +import { ProfileHeader } from "./ProfileHeader"; import { SOC2CostConsent } from "./SOC2CostConsent"; import { VendorChecksList } from "./VendorChecksList"; @@ -46,29 +42,8 @@ const ComplianceDashboard: React.FC = () => { <> - - - - { - window.open( - "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST", - "_blank" - ); - }} - > - AWS SOC 2 Controls (Vanta) - - - - + diff --git a/dashboard/src/main/home/compliance-dashboard/ConfigSelectors.tsx b/dashboard/src/main/home/compliance-dashboard/ConfigSelectors.tsx index 4c5622cb1c..0dfa7ddaba 100644 --- a/dashboard/src/main/home/compliance-dashboard/ConfigSelectors.tsx +++ b/dashboard/src/main/home/compliance-dashboard/ConfigSelectors.tsx @@ -11,22 +11,30 @@ import provider from "assets/provider.svg"; import typeSvg from "assets/type.svg"; import vanta from "assets/vanta.svg"; +import { useCompliance } from "./ComplianceContext"; + export const ConfigSelectors: React.FC = () => { - // to be made selectable with state living in context + const { profile, setProfile } = useCompliance(); return (