Przeglądaj źródła

compliance dash core functionality (#4179)

Co-authored-by: jusrhee <justin@porter.run>
ianedwards 2 lat temu
rodzic
commit
4788ca0c01

+ 6 - 2
dashboard/src/components/porter/Select.tsx

@@ -42,8 +42,12 @@ const Select: React.FC<Props> = ({
       {label && <Label color={labelColor}>{label}</Label>}
       <SelectWrapper>
         <AbsoluteWrapper>
-          <Prefix>{prefix}</Prefix>
-          <Bar />
+          {prefix && (
+            <>
+              <Prefix>{prefix}</Prefix>
+              <Bar />
+            </>
+          )}
           {options.map((option) => {
             if (option.value === value) {
               return (

+ 160 - 0
dashboard/src/main/home/compliance-dashboard/ActionBanner.tsx

@@ -0,0 +1,160 @@
+import React, { useMemo, type Dispatch, type SetStateAction } from "react";
+import { useHistory } from "react-router";
+
+import Banner from "components/porter/Banner";
+import Image from "components/porter/Image";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+
+import loading_img from "assets/loading.gif";
+import refresh from "assets/refresh.png";
+
+import { useCompliance } from "./ComplianceContext";
+
+type ActionBannerProps = {
+  setShowCostConsentModal: Dispatch<SetStateAction<boolean>>;
+};
+
+export const ActionBanner: React.FC<ActionBannerProps> = ({
+  setShowCostConsentModal,
+}) => {
+  const history = useHistory();
+  const {
+    updateInProgress,
+    latestContractDB,
+    latestContractProto,
+    updateContractWithSOC2,
+  } = useCompliance();
+
+  const provisioningStatus = useMemo(() => {
+    if (!latestContractDB || latestContractDB.condition === "") {
+      return {
+        state: "pending" as const,
+        message: latestContractDB?.condition_metadata?.message ?? "",
+      };
+    }
+
+    if (latestContractDB.condition === "SUCCESS") {
+      return {
+        state: "success" as const,
+        message: latestContractDB.condition_metadata?.message ?? "",
+      };
+    }
+
+    if (latestContractDB.condition === "COMPLIANCE_CHECK_FAILED") {
+      return {
+        state: "compliance_error" as const,
+        message: latestContractDB.condition_metadata?.message ?? "",
+      };
+    }
+
+    return {
+      state: "failed" as const,
+      message: latestContractDB.condition_metadata?.message ?? "",
+    };
+  }, [latestContractDB?.condition]);
+
+  // check if provisioning is pending
+  const isInfraPending = useMemo(() => {
+    return provisioningStatus.state === "pending" || updateInProgress;
+  }, [provisioningStatus.state, updateInProgress]);
+
+  // 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, latestContractProto?.toJsonString()]);
+
+  // check if provisioning error is due to compliance update
+  const provisioningErrorWithComplianceEnabled = useMemo(() => {
+    return (
+      provisioningStatus.state === "compliance_error" &&
+      latestContractProto?.cluster?.isSoc2Compliant
+    );
+  }, [provisioningStatus.state, latestContractProto?.toJsonString()]);
+
+  if (isInfraPending) {
+    return (
+      <Banner
+        icon={
+          <Image src={loading_img} style={{ height: "16px", width: "16px" }} />
+        }
+      >
+        SOC 2 infrastructure controls are being enabled. Note: This may take up
+        to 30 minutes.
+      </Banner>
+    );
+  }
+
+  if (actionRequredWithoutProvisioningError) {
+    return (
+      <Banner type="warning">
+        Action is required to pass additional controls.
+        <Spacer inline x={0.5} />
+        {provisioningStatus.state === "compliance_error" ? (
+          <Text
+            style={{
+              textDecoration: "underline",
+              cursor: "pointer",
+            }}
+            onClick={() => {
+              void updateContractWithSOC2();
+            }}
+          >
+            Re-run infrastructure controls
+          </Text>
+        ) : (
+          <Text
+            style={{
+              textDecoration: "underline",
+              cursor: "pointer",
+            }}
+            onClick={() => {
+              setShowCostConsentModal(true);
+            }}
+          >
+            Enable SOC 2 infrastructure controls
+          </Text>
+        )}
+      </Banner>
+    );
+  }
+
+  if (provisioningErrorWithComplianceEnabled) {
+    return (
+      <Banner type="error">
+        An error occurred while applying updates to your infrastructure.
+        <Spacer inline x={1} />
+        <Text
+          style={{
+            textDecoration: "underline",
+            cursor: "pointer",
+          }}
+          onClick={() => {
+            history.push("/cluster-dashboard");
+          }}
+        >
+          Learn more
+        </Text>
+        <Spacer inline x={1} />
+        <Text
+          style={{
+            textDecoration: "underline",
+            cursor: "pointer",
+          }}
+          onClick={() => {
+            void updateContractWithSOC2();
+          }}
+        >
+          <Image src={refresh} size={12} style={{ marginBottom: "-2px" }} />
+          <Spacer inline x={0.5} />
+          Retry update
+        </Text>
+      </Banner>
+    );
+  }
+
+  return null;
+};

+ 184 - 0
dashboard/src/main/home/compliance-dashboard/ComplianceContext.tsx

@@ -0,0 +1,184 @@
+import React, { createContext, useContext, useMemo, useState } from "react";
+import { Contract, EKS, EKSLogging } from "@porter-dev/api-contracts";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { match } from "ts-pattern";
+import { z } from "zod";
+
+import api from "shared/api";
+
+import {
+  checkGroupValidator,
+  contractValidator,
+  vendorCheckValidator,
+  type APIContract,
+  type CheckGroup,
+  type VendorCheck,
+} from "./types";
+
+type ProjectComplianceContextType = {
+  projectId: number;
+  clusterId: number;
+  checkGroups: CheckGroup[];
+  vendorChecks: VendorCheck[];
+  latestContractProto: Contract | null;
+  latestContractDB?: APIContract;
+  checksLoading: boolean;
+  contractLoading: boolean;
+  updateInProgress: boolean;
+  updateContractWithSOC2: () => Promise<void>;
+};
+
+const ProjectComplianceContext =
+  createContext<ProjectComplianceContextType | null>(null);
+
+export const useCompliance = (): ProjectComplianceContextType => {
+  const context = useContext(ProjectComplianceContext);
+  if (!context) {
+    throw new Error(
+      "useCompliance must be used within a ProjectComplianceProvider"
+    );
+  }
+  return context;
+};
+
+type ProjectComplianceProviderProps = {
+  projectId: number;
+  clusterId: number;
+  children: React.ReactNode;
+};
+
+export const ProjectComplianceProvider: React.FC<
+  ProjectComplianceProviderProps
+> = ({ projectId, clusterId, children }) => {
+  const queryClient = useQueryClient();
+  const [updateInProgress, setUpdateInProgress] = useState(false);
+
+  const { data: baseContract, isLoading: contractLoading } = useQuery(
+    [projectId, clusterId, "getContracts"],
+    async () => {
+      const res = await api.getContracts(
+        "<token>",
+        {},
+        { project_id: projectId }
+      );
+
+      const data = await z.array(contractValidator).parseAsync(res.data);
+
+      return data.filter((contract) => contract.cluster_id === clusterId)[0];
+    },
+    {
+      refetchInterval: 3000,
+    }
+  );
+
+  const {
+    data: { checkGroups = [], vendorChecks = [] } = {},
+    isLoading: checksLoading,
+  } = useQuery(
+    [
+      {
+        projectId,
+        clusterId,
+        condition: baseContract?.condition ?? "",
+        name: "getComplianceChecks",
+      },
+    ],
+    async () => {
+      const res = await api.getComplianceChecks(
+        "<token>",
+        { vendor: "vanta" },
+        { projectId, clusterId }
+      );
+
+      const data = await z
+        .object({
+          check_groups: z.array(checkGroupValidator).optional().default([]),
+          vendor_checks: z.array(vendorCheckValidator).optional().default([]),
+        })
+        .parseAsync(res.data);
+
+      return {
+        checkGroups: data.check_groups,
+        vendorChecks: data.vendor_checks,
+      };
+    }
+  );
+
+  const latestContract = useMemo(() => {
+    if (!baseContract) {
+      return null;
+    }
+
+    return Contract.fromJsonString(atob(baseContract.base64_contract), {
+      ignoreUnknownFields: true,
+    });
+  }, [baseContract?.base64_contract]);
+
+  const updateContractWithSOC2 = async (): Promise<void> => {
+    try {
+      setUpdateInProgress(true);
+
+      if (!latestContract?.cluster) {
+        return;
+      }
+
+      const updatedKindValues = match(latestContract.cluster.kindValues)
+        .with({ case: "eksKind" }, ({ value }) => ({
+          case: "eksKind" as const,
+          value: new EKS({
+            ...value,
+            enableKmsEncryption: true,
+            enableEcrScanning: true,
+            logging: new EKSLogging({
+              enableApiServerLogs: true,
+              enableAuditLogs: true,
+              enableAuthenticatorLogs: true,
+              enableCloudwatchLogsToS3: true,
+              enableControllerManagerLogs: true,
+              enableSchedulerLogs: true,
+            }),
+          }),
+        }))
+        .otherwise((kind) => kind);
+
+      const updatedContract = new Contract({
+        ...latestContract,
+        cluster: {
+          ...latestContract.cluster,
+          kindValues: updatedKindValues,
+          isSoc2Compliant: true,
+        },
+      });
+
+      await api.createContract("<token>", updatedContract, {
+        project_id: projectId,
+      });
+      await queryClient.invalidateQueries([
+        projectId,
+        clusterId,
+        "getContracts",
+      ]);
+    } finally {
+      setUpdateInProgress(false);
+    }
+  };
+
+  return (
+    <ProjectComplianceContext.Provider
+      value={{
+        projectId,
+        clusterId,
+        vendorChecks,
+        checkGroups,
+        latestContractProto: latestContract,
+        latestContractDB: baseContract,
+        checksLoading,
+        contractLoading,
+        updateInProgress,
+        updateContractWithSOC2,
+      }}
+    >
+      {children}
+    </ProjectComplianceContext.Provider>
+  );
+};

+ 61 - 496
dashboard/src/main/home/compliance-dashboard/ComplianceDashboard.tsx

@@ -1,533 +1,98 @@
-import React, { useState } from "react";
-import _, { set } from "lodash";
+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 Container from "components/porter/Container";
-
-import Text from "components/porter/Text";
-import Select from "components/porter/Select";
-import Image from "components/porter/Image";
-import Banner from "components/porter/Banner";
-import Modal from "components/porter/Modal";
-import ExpandableSection from "components/porter/ExpandableSection";
-import Fieldset from "components/porter/Fieldset";
-import Link from "components/porter/Link";
-import Input from "components/porter/Input";
-import Button from "components/porter/Button";
-
-import framework from "assets/framework.svg";
-import typeSvg from "assets/type.svg";
-import provider from "assets/provider.svg";
-import aws from "assets/aws.png";
-import vanta from "assets/vanta.svg";
 import linkExternal from "assets/link-external.svg";
-import greenCheck from "assets/green-check.svg";
-import warning from "assets/warning.svg";
-import notApplicable from "assets/not-applicable.svg";
-import loading from "assets/loading.gif";
-import refresh from "assets/refresh.png";
+import vanta from "assets/vanta.svg";
 
-type Props = {
-  projectId: number;
-};
+import { ActionBanner } from "./ActionBanner";
+import { ProjectComplianceProvider } from "./ComplianceContext";
+import { ConfigSelectors } from "./ConfigSelectors";
+import { SOC2CostConsent } from "./SOC2CostConsent";
+import { VendorChecksList } from "./VendorChecksList";
 
-const dummyChecks = [
-  {
-    status: "not-applicable",
-    name: "Application changes reviewed",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "not-applicable",
-    name: "AWS accounts deprovisioned when employees leave",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "not-applicable",
-    name: "AWS accounts reviewed",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "passing",
-    name: "CloudTrail enabled",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "not-applicable",
-    name: "Company has a version control system",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "not-applicable",
-    name: "Critical vulnerabilities identified in packages are addressed (AWS Container)",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "not-applicable",
-    name: "Critical vulnerabilities identified in packages are addressed (AWS Inspector)",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "action-required",
-    name: "Database IO monitored (AWS)",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "passing",
-    name: "CloudTrail enabled",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "not-applicable",
-    name: "Company has a version control system",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "not-applicable",
-    name: "Critical vulnerabilities identified in packages are addressed (AWS Container)",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "not-applicable",
-    name: "Critical vulnerabilities identified in packages are addressed (AWS Inspector)",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-  {
-    status: "action-required",
-    name: "Database IO monitored (AWS)",
-    link: "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
-  },
-];
+const ComplianceDashboard: React.FC = () => {
+  const { currentProject, currentCluster } = useContext(Context);
 
-const ComplianceDashboard: React.FC<Props> = () => {
-  const [actionRequired, setActionRequired] = useState(true); // TODO: replace with actual data
-  const [provisioningError, setProvisioningError] = useState(""); // TODO: replace with actual data
-  const [provisioningStatus, setProvisioningStatus] = useState("");
-  const [statusFilter, setStatusFilter] = useState("all");
-  const [confirmCost, setConfirmCost] = useState("");
   const [showCostConsentModal, setShowCostConsentModal] = useState(false);
-  const [showExpandedErrorModal, setShowExpandedErrorModal] = useState(false);
-  const [expandedCheck, setExpandedCheck] = useState<{ 
-    status: string, name: string, link: string 
-  } | null>(null);
 
-  // TODO: implement
-  const updateInfrastructure = (): void => {
-    setProvisioningError("");
-    setProvisioningStatus("pending");
-
-    setTimeout(() => {
-      setProvisioningStatus("failed");
-      setProvisioningError("Error: Some step failed");
-    }, 2000);
-  };
+  if (!currentProject || !currentCluster) {
+    return null;
+  }
 
   return (
-    <StyledComplianceDashboard>
-      <DashboardHeader
-        image={compliance}
-        title="Compliance"
-        description="Configure your Porter infrastructure for various compliance frameworks."
-        disableLineBreak
-      />
-      <Container row>
-        <Select
-          options={[
-            { value: "soc-2", label: "SOC 2" },
-            { value: "hipaa", label: "HIPAA (request access)", disabled: true },
-          ]}
-          width="200px"
-          value={"soc-2"}
-          setValue={() => {
-          }
-          }
-          prefix={
-            <Container row>
-              <Image src={framework} size={15} opacity={0.6} />
-              <Spacer inline x={0.5} />
-              Framework
-            </Container>
-          }
-        />
-        <Spacer inline x={1} />
-        <Select
-          options={[
-            { value: "aws", label: "AWS", icon: aws },
-            { value: "gcp", label: "Google Cloud (coming soon)", disabled: true },
-            { value: "azure", label: "Azure (coming soon)", disabled: true },
-          ]}
-          width="180px"
-          value={"aws"}
-          setValue={() => {
-          }
-          }
-          prefix={
-            <Container row>
-              <Image src={typeSvg} size={15} opacity={0.6} />
-              <Spacer inline x={0.5} />
-              Type
-            </Container>
-          }
-        />
-        <Spacer inline x={1} />
-        <Select
-          options={[
-            { value: "vanta", label: "Vanta", icon: vanta },
-            { value: "drata", label: "Drata (coming soon)", disabled: true },
-            { value: "oneleet", label: "Oneleet (coming soon)", disabled: true },
-          ]}
-          width="200px"
-          value={"vanta"}
-          setValue={() => {
-          }
-          }
-          prefix={
-            <Container row>
-              <Image src={provider} size={15} opacity={.6} />
-              <Spacer inline x={.5} />
-              Provider
-            </Container>
-          }
+    <ProjectComplianceProvider
+      projectId={currentProject.id}
+      clusterId={currentCluster.id}
+    >
+      <StyledComplianceDashboard>
+        <DashboardHeader
+          image={compliance}
+          title="Compliance"
+          description="Configure your Porter infrastructure for various compliance frameworks."
+          disableLineBreak
         />
-      </Container>
-
-      <Spacer y={1} />
-
-      <Container row>
-        <Image src={vanta} size={25} />
-        <Spacer inline x={1} />
-        <Text 
-          size={21}
-          additionalStyles=":hover { text-decoration: underline } cursor: pointer;"
-          onClick={() => {
-            window.open("https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST", "_blank")
-          }}
-        >
-          AWS SOC 2 Controls (Vanta)
-          <Spacer inline x={.5} />
-          <Image src={linkExternal} size={16} additionalStyles="margin-bottom: -2px"/>
-        </Text>
-      </Container>
-
-      <Spacer y={1} />
-
-      {
-        actionRequired &&
-        provisioningStatus !== "pending" &&
-        provisioningStatus !== "failed" && (
+        {currentCluster?.status === "UPDATING_UNAVAILABLE" ? (
+          <ClusterProvisioningPlaceholder />
+        ) : (
           <>
-            <Banner type="warning">
-              Action is required to pass additional controls.
-              <Spacer inline x={.5} />
-              <Text
-                style={{
-                  textDecoration: "underline",
-                  cursor: "pointer"
-                }}
-                onClick={() => {
-                  setShowCostConsentModal(true);
-                }}
-              >
-                Enable SOC 2 infrastructure controls
-              </Text>
-            </Banner>
+            <ConfigSelectors />
             <Spacer y={1} />
-          </>
-        )
-      }
-      {provisioningStatus === "pending" && (
-        <>
-          <Banner icon={<Image src={loading} style={{ height: "16px", width: "16px" }} />}>
-            SOC 2 infrastructure controls are being enabled. Note: This may take up to 30 minutes.
-          </Banner>
-          <Spacer y={1} />
-        </>
-      )}
-      {provisioningError && (
-        <>
-          <Banner type="error">
-            {provisioningError}
-            <Spacer inline x={1} />
-            <Text
-              style={{
-                textDecoration: "underline",
-                cursor: "pointer"
-              }}
-              onClick={() => {
-                setShowExpandedErrorModal(true);
-              }}
-            >
-              Learn more
-            </Text>
-            <Spacer inline x={1} />
-            <Text
-              style={{
-                textDecoration: "underline",
-                cursor: "pointer"
-              }}
-              onClick={updateInfrastructure}
-            >
-              <Image src={refresh} size={12} style={{ marginBottom: "-2px" }} />
-              <Spacer inline x={.5} />
-              Retry update
-            </Text>
-          </Banner>
-          <Spacer y={1} />
-        </>
-      )}
-
-      <Container row>
-        <PanelFilter
-          isActive={statusFilter === "all"}
-          onClick={() => {
-            setStatusFilter("all");
-          }}
-        >
-          <Text color="helper">All</Text>
-          <Spacer y={.2} />
-          <Text size={18}>45</Text>
-        </PanelFilter>
-        <Spacer inline x={1.5} />
-        <PanelFilter
-          isActive={statusFilter === "passing"}
-          onClick={() => {
-            setStatusFilter("passing");
-          }}
-        >
-          <Container row>
-            <Image src={greenCheck} size={10} />
-            <Spacer inline x={.5} />
-            <Text color="helper">Passing</Text>
-          </Container>
-          <Spacer y={.2} />
-          <Text size={18}>3</Text>
-        </PanelFilter>
-        <Spacer inline x={1.5} />
-        <PanelFilter
-          isActive={statusFilter === "action-required"}
-          onClick={() => {
-            setStatusFilter("action-required");
-          }}
-        >
-          <Container row>
-            <Image src={warning} size={12} />
-            <Spacer inline x={.5} />
-            <Text color="helper">Action required</Text>
-          </Container>
-          <Spacer y={.2} />
-          <Text size={18}>17</Text>
-        </PanelFilter>
-        <Spacer inline x={1.5} />
-        <PanelFilter
-          isActive={statusFilter === "not-applicable"}
-          onClick={() => {
-            setStatusFilter("not-applicable");
-          }}
-        >
-          <Container row>
-            <Image src={notApplicable} size={12} />
-            <Spacer inline x={.5} />
-            <Text color="helper">Not applicable</Text>
-          </Container>
-          <Spacer y={.2} />
-          <Text size={18}>25</Text>
-        </PanelFilter>
-      </Container>
-
-      <Spacer y={1.5} />
-
-      {dummyChecks.map((check, i) => {
-        return (
-          <>
-            <Container row key={i}>
-              <Container style={{ width: "200px" }} row>
-                {check.status === "passing" && <Image src={greenCheck} size={10} />}
-                {check.status === "action-required" && (
-                  <Image src={warning} size={14} />
-                )}
-                {check.status === "not-applicable" && (
-                  <Image src={notApplicable} size={14} />
-                )}
-                <Spacer inline x={.7} />
-                {check.status === "passing" && <Text color="helper">Passing</Text>}
-                {check.status === "action-required" && (
-                  <ActionRequired>
-                    <Text color="helper">Action required</Text>
-                    <Spacer inline x={.5} />
-                    <i 
-                      className="material-icons-outlined"
-                      onClick={() => { setExpandedCheck(check) }}
-                    >
-                      help_outline
-                    </i>
-                  </ActionRequired>
-                )}
-                {check.status === "not-applicable" && <Text color="#494B4F">Not applicable</Text>}
-              </Container>
+            <Container row>
+              <Image src={vanta} size={25} />
+              <Spacer inline x={1} />
               <Text
-                color={check.status === "not-applicable" ? "#494B4F" : ""}
-                style={{ 
-                  marginBottom: "-1px",
-                  cursor: "pointer",
-                }}
-                additionalStyles=":hover { text-decoration: underline }"
+                size={21}
+                additionalStyles=":hover { text-decoration: underline } cursor: pointer;"
                 onClick={() => {
-                  window.open(check.link, "_blank");
+                  window.open(
+                    "https://app.vanta.com/tests?framework=soc2&service=aws&taskType=TEST",
+                    "_blank"
+                  );
                 }}
               >
-                {check.name}
-                <Spacer inline x={.5} />
-                <Image 
+                AWS SOC 2 Controls (Vanta)
+                <Spacer inline x={0.5} />
+                <Image
                   src={linkExternal}
-                  opacity={check.status === "not-applicable" ? 0.25 : 1}
-                  size={12}
+                  size={16}
                   additionalStyles="margin-bottom: -2px"
                 />
               </Text>
             </Container>
+
             <Spacer y={1} />
-          </>
-        );
-      })}
 
-      <Spacer y={2} />
+            <ActionBanner setShowCostConsentModal={setShowCostConsentModal} />
+            <Spacer y={1} />
+
+            <VendorChecksList />
 
-      {showExpandedErrorModal && (
-        <Modal closeModal={() => { setShowExpandedErrorModal(false) }}>
-          <Container row>
-            <Text size={16}>
-              Error enabling AWS SOC 2 controls
-            </Text>
-          </Container>
-          <Spacer y={.7} />
-          <Text color="helper">
-            {provisioningError}
-          </Text>
-        </Modal>
-      )}
-      {expandedCheck && (
-        <Modal closeModal={() => { setExpandedCheck(null) }}>
-          <Container row>
-            <Image src={warning} size={16} />
-            <Spacer inline x={.7} />
-            <Text size={16}>
-              Action required for "{expandedCheck.name}"
-            </Text>
-          </Container>
-          <Spacer y={.7} />
-          <Text color="helper">
-            Porter is unable to automatically resolve this control. Please follow xyz instructions in order to xyz.
-          </Text>
-        </Modal>
-      )}
-      {showCostConsentModal && (
-        <Modal closeModal={() => { setShowCostConsentModal(false) }}>
-          <Text size={16}>SOC 2 cost consent (TODO)</Text>
-          <Spacer height="15px" />
-          <Text color="helper">
-            Porter will create the underlying infrastructure in your own AWS
-            account. You will be separately charged by AWS for this
-            infrastructure. The cost for this base infrastructure is as follows:
-          </Text>
-          <Spacer y={1} />
-          <ExpandableSection
-            noWrapper
-            expandText="[+] Show details"
-            collapseText="[-] Hide details"
-            Header={<Text size={20} weight={600}>$224.58 / mo</Text>}
-            ExpandedSection={
-              <>
-                <Spacer height="15px" />
-                <Fieldset background="#1b1d2688">
-                  • Amazon Elastic Kubernetes Service (EKS) = $73/mo
-                  <Spacer height="15px" />
-                  • Amazon EC2:
-                  <Spacer height="15px" />
-                  <Tab />+ System workloads: t3.medium instance (2) = $60.74/mo
-                  <Spacer height="15px" />
-                  <Tab />+ Monitoring workloads: t3.large instance (1) = $60.74/mo
-                  <Spacer height="15px" />
-                  <Tab />+ Application workloads: t3.medium instance (1) =
-                  $30.1/mo
-                </Fieldset>
-              </>
-            }
-          />
-          <Spacer y={1} />
-          <Text color="helper">
-            The base AWS infrastructure covers up to 2 vCPU and 4GB of RAM.
-            Separate from the AWS cost, Porter charges based on your resource
-            usage.
-          </Text>
-          <Spacer inline width="5px" />
-          <Spacer y={0.5} />
-          <Link hasunderline to="https://porter.run/pricing" target="_blank">
-            Learn more about our pricing.
-          </Link>
-          <Spacer y={1} />
-          <Input
-            placeholder="224.58"
-            value={confirmCost}
-            setValue={setConfirmCost}
-            width="100%"
-            height="40px"
-          />
-          <Spacer y={1} />
-          <Button
-            disabled={confirmCost !== "224.58"}
-            onClick={() => {
-              setConfirmCost("");
-              updateInfrastructure();
-              setShowCostConsentModal(false);
-            }}
-          >
-            Enable SOC 2 infra controls
-          </Button>
-        </Modal>
-      )}
-    </StyledComplianceDashboard>
+            <Spacer y={2} />
+
+            {showCostConsentModal && (
+              <SOC2CostConsent
+                setShowCostConsentModal={setShowCostConsentModal}
+              />
+            )}
+          </>
+        )}
+      </StyledComplianceDashboard>
+    </ProjectComplianceProvider>
   );
 };
 
 export default ComplianceDashboard;
 
-const ActionRequired = styled.div`
-  > i {
-    font-size: 15px;
-    color: #aaaabb;
-    cursor: pointer;
-    :hover {
-      color: #ffffff;
-    }
-  }
-  display: flex;
-  align-items: center;
-`;
-
-const Tab = styled.span`
-  margin-left: 20px;
-  height: 1px;
-`;
-
-const PanelFilter = styled.div<{ isActive: boolean }>`
-  display: flex;
-  flex-direction: column;
-  flex: 1;
-  padding: 10px 15px;
-  cursor: pointer;
-  border-radius: 5px;
-  background: ${(props) => props.theme.clickable.bg};
-  border: 1px solid ${(props) => props.isActive ? "#fefefe" : "#494b4f"};
-  :hover {
-    ${(props) => !props.isActive && "border: 1px solid #7a7b80;"}
-  }
-`;
-
 const StyledComplianceDashboard = styled.div`
   width: 100%;
   height: 100%;
-`;
+`;

+ 92 - 0
dashboard/src/main/home/compliance-dashboard/ConfigSelectors.tsx

@@ -0,0 +1,92 @@
+import React from "react";
+
+import Container from "components/porter/Container";
+import Image from "components/porter/Image";
+import Select from "components/porter/Select";
+import Spacer from "components/porter/Spacer";
+
+import aws from "assets/aws.png";
+import framework from "assets/framework.svg";
+import provider from "assets/provider.svg";
+import typeSvg from "assets/type.svg";
+import vanta from "assets/vanta.svg";
+
+export const ConfigSelectors: React.FC = () => {
+  // to be made selectable with state living in context
+  return (
+    <Container row>
+      <Select
+        options={[
+          { value: "soc-2", label: "SOC 2" },
+          {
+            value: "hipaa",
+            label: "HIPAA (request access)",
+            disabled: true,
+          },
+        ]}
+        width="200px"
+        value={"soc-2"}
+        setValue={() => {}}
+        prefix={
+          <Container row>
+            <Image src={framework} size={15} opacity={0.6} />
+            <Spacer inline x={0.5} />
+            Framework
+          </Container>
+        }
+      />
+      <Spacer inline x={1} />
+      <Select
+        options={[
+          { value: "aws", label: "AWS", icon: aws },
+          {
+            value: "gcp",
+            label: "Google Cloud (coming soon)",
+            disabled: true,
+          },
+          {
+            value: "azure",
+            label: "Azure (coming soon)",
+            disabled: true,
+          },
+        ]}
+        width="180px"
+        value={"aws"}
+        setValue={() => {}}
+        prefix={
+          <Container row>
+            <Image src={typeSvg} size={15} opacity={0.6} />
+            <Spacer inline x={0.5} />
+            Type
+          </Container>
+        }
+      />
+      <Spacer inline x={1} />
+      <Select
+        options={[
+          { value: "vanta", label: "Vanta", icon: vanta },
+          {
+            value: "drata",
+            label: "Drata (coming soon)",
+            disabled: true,
+          },
+          {
+            value: "oneleet",
+            label: "Oneleet (coming soon)",
+            disabled: true,
+          },
+        ]}
+        width="200px"
+        value={"vanta"}
+        setValue={() => {}}
+        prefix={
+          <Container row>
+            <Image src={provider} size={15} opacity={0.6} />
+            <Spacer inline x={0.5} />
+            Provider
+          </Container>
+        }
+      />
+    </Container>
+  );
+};

+ 73 - 0
dashboard/src/main/home/compliance-dashboard/SOC2CostConsent.tsx

@@ -0,0 +1,73 @@
+import React, { type Dispatch, type SetStateAction } from "react";
+
+import Button from "components/porter/Button";
+import ExpandableSection from "components/porter/ExpandableSection";
+import Fieldset from "components/porter/Fieldset";
+import Modal from "components/porter/Modal";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+
+import { useCompliance } from "./ComplianceContext";
+
+type Props = {
+  setShowCostConsentModal: Dispatch<SetStateAction<boolean>>;
+};
+
+export const SOC2CostConsent: React.FC<Props> = ({
+  setShowCostConsentModal,
+}) => {
+  const { updateInProgress, updateContractWithSOC2 } = useCompliance();
+
+  return (
+    <Modal
+      closeModal={() => {
+        setShowCostConsentModal(false);
+      }}
+    >
+      <Text size={16}>Enable SOC-2</Text>
+      <Spacer height="15px" />
+      <Text color="helper">
+        Porter will update the underlying infrastructure in your own AWS account
+        to ensure compliance with any automated SOC-2 controls. Additional costs
+        may apply.
+      </Text>
+      <Spacer y={1} />
+      <ExpandableSection
+        noWrapper
+        expandText="[+] Show details"
+        collapseText="[-] Hide details"
+        Header={
+          <Text size={20} weight={600}>
+            Additional updates for SOC-2
+          </Text>
+        }
+        isInitiallyExpanded
+        ExpandedSection={
+          <>
+            <Spacer height="15px" />
+            <Fieldset background="#1b1d2688">
+              • GuardDuty (threat detection)
+              <Spacer height="15px" />
+              • CloudTrail (audit logs) and CloudWatch (monitoring)
+              <Spacer height="15px" />
+              • KMS encrypted secrets
+              <Spacer height="15px" />
+              • Container vulnerability scanning
+              <Spacer height="15px" />
+            </Fieldset>
+          </>
+        }
+      />
+      <Spacer y={1} />
+      <Button
+        onClick={() => {
+          void updateContractWithSOC2();
+        }}
+        status={updateInProgress ? "loading" : undefined}
+        disabled={updateInProgress}
+      >
+        Enable SOC 2 infra controls
+      </Button>
+    </Modal>
+  );
+};

+ 223 - 0
dashboard/src/main/home/compliance-dashboard/VendorChecksList.tsx

@@ -0,0 +1,223 @@
+import React, { Fragment, useState } from "react";
+import styled from "styled-components";
+
+import Container from "components/porter/Container";
+import Image from "components/porter/Image";
+import Modal from "components/porter/Modal";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import { useIntercom } from "lib/hooks/useIntercom";
+
+import greenCheck from "assets/green-check.svg";
+import linkExternal from "assets/link-external.svg";
+import notApplicable from "assets/not-applicable.svg";
+import warning from "assets/warning.svg";
+
+import { useCompliance } from "./ComplianceContext";
+import { type VendorCheck } from "./types";
+
+export const VendorChecksList: React.FC = () => {
+  const { vendorChecks, latestContractProto } = useCompliance();
+  const { showIntercomWithMessage } = useIntercom();
+
+  const [statusFilter, setStatusFilter] = useState("all");
+  const [expandedCheck, setExpandedCheck] = useState<VendorCheck | null>(null);
+
+  const renderExpandedCheckText = (): JSX.Element | null => {
+    if (!expandedCheck) {
+      return null;
+    }
+
+    if (!latestContractProto?.cluster?.isSoc2Compliant) {
+      return (
+        <Text color="helper">
+          SOC-2 compliance is not enabled for this cluster. Re-provisioning your
+          infrastructure above will enable necessary security controls to ensure
+          compliance.
+        </Text>
+      );
+    }
+
+    return (
+      <Text color="helper">
+        {expandedCheck.reason.length
+          ? expandedCheck.reason
+          : "An error occurred during provisioning that is causing this check to be unfulfilled. Please attempt to re-provision or contact support@porter.run if the error persists."}
+      </Text>
+    );
+  };
+
+  return (
+    <>
+      <Container row>
+        <PanelFilter
+          isActive={statusFilter === "all"}
+          onClick={() => {
+            setStatusFilter("all");
+          }}
+        >
+          <Text color="helper">All</Text>
+          <Spacer y={0.2} />
+          <Text size={18}>45</Text>
+        </PanelFilter>
+        <Spacer inline x={1.5} />
+        <PanelFilter
+          isActive={statusFilter === "passing"}
+          onClick={() => {
+            setStatusFilter("passing");
+          }}
+        >
+          <Container row>
+            <Image src={greenCheck} size={10} />
+            <Spacer inline x={0.5} />
+            <Text color="helper">Passing</Text>
+          </Container>
+          <Spacer y={0.2} />
+          <Text size={18}>3</Text>
+        </PanelFilter>
+        <Spacer inline x={1.5} />
+        <PanelFilter
+          isActive={statusFilter === "action-required"}
+          onClick={() => {
+            setStatusFilter("action-required");
+          }}
+        >
+          <Container row>
+            <Image src={warning} size={12} />
+            <Spacer inline x={0.5} />
+            <Text color="helper">Action required</Text>
+          </Container>
+          <Spacer y={0.2} />
+          <Text size={18}>17</Text>
+        </PanelFilter>
+        <Spacer inline x={1.5} />
+        <PanelFilter
+          isActive={statusFilter === "not-applicable"}
+          onClick={() => {
+            setStatusFilter("not-applicable");
+          }}
+        >
+          <Container row>
+            <Image src={notApplicable} size={12} />
+            <Spacer inline x={0.5} />
+            <Text color="helper">Not applicable</Text>
+          </Container>
+          <Spacer y={0.2} />
+          <Text size={18}>25</Text>
+        </PanelFilter>
+      </Container>
+
+      <Spacer y={1.5} />
+
+      {vendorChecks.map((check, i) => {
+        return (
+          <Fragment key={`${check.check}-${i}`}>
+            <Container row>
+              <Container style={{ width: "200px" }} row>
+                {check.status === "passed" && (
+                  <Image src={greenCheck} size={10} />
+                )}
+                {check.status === "failing" && (
+                  <Image src={warning} size={14} />
+                )}
+                {check.status === "not_applicable" && (
+                  <Image src={notApplicable} size={14} />
+                )}
+                <Spacer inline x={0.7} />
+                {check.status === "passed" && (
+                  <Text color="helper">Passing</Text>
+                )}
+                {check.status === "failing" && (
+                  <ActionRequired>
+                    <Text color="helper">Action required</Text>
+                    <Spacer inline x={0.5} />
+                    <i
+                      className="material-icons-outlined"
+                      onClick={() => {
+                        setExpandedCheck(check);
+                      }}
+                    >
+                      help_outline
+                    </i>
+                  </ActionRequired>
+                )}
+                {check.status === "not_applicable" && (
+                  <Text color="#494B4F">Not applicable</Text>
+                )}
+              </Container>
+              <Text
+                color={check.status === "not_applicable" ? "#494B4F" : ""}
+                style={{
+                  marginBottom: "-1px",
+                  cursor: "pointer",
+                }}
+                additionalStyles=":hover { text-decoration: underline }"
+                onClick={() => {
+                  window.open(check.link, "_blank");
+                }}
+              >
+                {check.check}
+                <Spacer inline x={0.5} />
+                <Image
+                  src={linkExternal}
+                  opacity={check.status === "not_applicable" ? 0.25 : 1}
+                  size={12}
+                  additionalStyles="margin-bottom: -2px"
+                />
+              </Text>
+            </Container>
+            <Spacer y={1} />
+          </Fragment>
+        );
+      })}
+
+      {expandedCheck && (
+        <Modal
+          closeModal={() => {
+            showIntercomWithMessage({
+              message: "I am running into an issue enabling SOC-2 compliance.",
+            });
+            setExpandedCheck(null);
+          }}
+        >
+          <Container row>
+            <Image src={warning} size={16} />
+            <Spacer inline x={0.7} />
+            <Text size={16}>
+              Action required for &ldquo;{expandedCheck.check}&rdquo;
+            </Text>
+          </Container>
+          <Spacer y={0.7} />
+          {renderExpandedCheckText()}
+        </Modal>
+      )}
+    </>
+  );
+};
+
+const PanelFilter = styled.div<{ isActive: boolean }>`
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  padding: 10px 15px;
+  cursor: pointer;
+  border-radius: 5px;
+  background: ${(props) => props.theme.clickable.bg};
+  border: 1px solid ${(props) => (props.isActive ? "#fefefe" : "#494b4f")};
+  :hover {
+    ${(props) => !props.isActive && "border: 1px solid #7a7b80;"}
+  }
+`;
+
+const ActionRequired = styled.div`
+  > i {
+    font-size: 15px;
+    color: #aaaabb;
+    cursor: pointer;
+    :hover {
+      color: #ffffff;
+    }
+  }
+  display: flex;
+  align-items: center;
+`;

+ 78 - 0
dashboard/src/main/home/compliance-dashboard/types.ts

@@ -0,0 +1,78 @@
+import { z } from "zod";
+
+export const checkGroupValidator = z.object({
+  name: z.string(),
+  status: z.enum(["PASSED", "FAILED"]),
+  message: z.string().optional().default(""),
+});
+export type CheckGroup = z.infer<typeof checkGroupValidator>;
+
+export const vendorCheckValidator = z.object({
+  check: z.string(),
+  check_group: z.string(),
+  status: z.enum(["passed", "failing", "not_applicable"]),
+  reason: z.string(),
+  link: z.string().optional(),
+});
+export type VendorCheck = z.infer<typeof vendorCheckValidator>;
+
+export const contractValidator = z.object({
+  id: z.string(),
+  base64_contract: z.string(),
+  cluster_id: z.number(),
+  project_id: z.number(),
+  condition: z.enum([
+    "",
+    "QUOTA_REQUEST_FAILED",
+    "RETRYING_TOO_LONG",
+    "KUBE_APPLY_FAILED",
+    "FATAL_PROVISIONING_ERROR",
+    "ERROR_READING_MSG",
+    "MSG_CAUSED_PANIC",
+    "SUCCESS",
+    "DELETING",
+    "DELETED",
+    "COMPLIANCE_CHECK_FAILED",
+  ]),
+  condition_metadata: z
+    .discriminatedUnion("code", [
+      z.object({
+        code: z.literal("SUCCESS"),
+        message: z.string().optional(),
+      }),
+      z.object({
+        code: z.literal("COMPLIANCE_CHECK_FAILED"),
+        message: z.string().optional(),
+        metadata: z.object({
+          check_groups: z.array(checkGroupValidator),
+        }),
+      }),
+      // all other codes are just "code" and "message"
+      z.object({
+        code: z.literal("QUOTA_REQUEST_FAILED"),
+        message: z.string().optional(),
+      }),
+      z.object({
+        code: z.literal("RETRYING_TOO_LONG"),
+        message: z.string().optional(),
+      }),
+      z.object({
+        code: z.literal("KUBE_APPLY_FAILED"),
+        message: z.string().optional(),
+      }),
+      z.object({
+        code: z.literal("FATAL_PROVISIONING_ERROR"),
+        message: z.string().optional(),
+      }),
+      z.object({
+        code: z.literal("ERROR_READING_MSG"),
+        message: z.string().optional(),
+      }),
+      z.object({
+        code: z.literal("MSG_CAUSED_PANIC"),
+        message: z.string().optional(),
+      }),
+    ])
+    .nullable(),
+});
+export type APIContract = z.infer<typeof contractValidator>;

+ 8 - 0
dashboard/src/shared/api.tsx

@@ -1565,6 +1565,13 @@ const getClusterState = baseApi<{}, { project_id: number; cluster_id: number }>(
   }
 );
 
+const getComplianceChecks = baseApi<
+  { vendor: "vanta" },
+  { projectId: number; clusterId: number }
+>("GET", ({ projectId, clusterId }) => {
+  return `/api/projects/${projectId}/clusters/${clusterId}/compliance/checks`;
+});
+
 const provisionInfra = baseApi<
   {
     kind: string;
@@ -3539,6 +3546,7 @@ export default {
   getMatchingPods,
   getAllReleasePods,
   getClusterState,
+  getComplianceChecks,
   getMetrics,
   appMetrics,
   appHelmValues,

+ 4 - 0
internal/models/api_contract_revision.go

@@ -3,6 +3,7 @@ package models
 import (
 	"database/sql/driver"
 	"encoding/json"
+	"time"
 
 	"github.com/google/uuid"
 	"gorm.io/gorm"
@@ -44,6 +45,9 @@ type APIContractRevision struct {
 	// 	]
 	// }
 	ConditionMetadata JSONB `json:"condition_metadata" sql:"type:jsonb" gorm:"type:jsonb"`
+
+	CreatedAt time.Time `json:"created_at"`
+	UpdatedAt time.Time `json:"updated_at"`
 }
 
 // TableName overrides the table name

+ 2 - 2
internal/repository/gorm/api_contract.go

@@ -39,13 +39,13 @@ func (cr APIContractRepository) List(ctx context.Context, projectID uint, cluste
 	var confs []*models.APIContractRevision
 
 	if clusterID == 0 {
-		tx := cr.db.Where("project_id = ?", projectID).Find(&confs).Order("created_at desc")
+		tx := cr.db.Where("project_id = ?", projectID).Order("created_at desc").Find(&confs)
 		if tx.Error != nil {
 			return nil, tx.Error
 		}
 		return confs, nil
 	}
-	tx := cr.db.Where("project_id = ? and cluster_id = ?", projectID, clusterID).Find(&confs).Order("created_at desc")
+	tx := cr.db.Where("project_id = ? and cluster_id = ?", projectID, clusterID).Order("created_at desc").Find(&confs)
 	if tx.Error != nil {
 		return nil, tx.Error
 	}