Merge pull request #95 from prowler-cloud/PRWLR-5363-Compliance-Overview

Compliance overview - first iteration -
This commit is contained in:
Pablo Lara
2024-11-18 14:17:08 +01:00
committed by GitHub
20 changed files with 346 additions and 544 deletions
+2 -2
View File
@@ -62,7 +62,7 @@ export const createNewUser = async (
const bodyData = {
data: {
type: "User",
type: "users",
attributes: {
name: formData.name,
email: formData.email,
@@ -99,7 +99,7 @@ export const getToken = async (formData: z.infer<typeof formSchemaSignIn>) => {
const bodyData = {
data: {
type: "Token",
type: "tokens",
attributes: {
email: formData.email,
password: formData.password,
-39
View File
@@ -1,39 +0,0 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { parseStringify } from "@/lib";
export const getCompliance = async ({ page = 1 }) => {
if (isNaN(Number(page)) || page < 1) redirect("/compliance");
const keyServer = process.env.SITE_URL;
try {
const compliance = await fetch(
`${keyServer}/api/compliance?page%5Bnumber%5D=${page}`,
);
const data = await compliance.json();
const parsedData = parseStringify(data);
revalidatePath("/compliance");
return parsedData;
} catch (error) {
console.error("Error fetching Compliance:", error);
return undefined;
}
};
export const getErrorMessage = (error: unknown): string => {
let message: string;
if (error instanceof Error) {
message = error.message;
} else if (error && typeof error === "object" && "message" in error) {
message = String(error.message);
} else if (typeof error === "string") {
message = error;
} else {
message = "Oops! Something went wrong.";
}
return message;
};
+43
View File
@@ -0,0 +1,43 @@
"use server";
import { revalidatePath } from "next/cache";
import { auth } from "@/auth.config";
import { parseStringify } from "@/lib";
export const getCompliancesOverview = async ({
scanId,
region,
}: {
scanId: string;
region?: string | string[];
}) => {
const session = await auth();
const keyServer = process.env.API_BASE_URL;
const url = new URL(`${keyServer}/compliance-overviews`);
if (scanId) url.searchParams.append("filter[scan_id]", scanId);
if (region) {
const regionValue = Array.isArray(region) ? region.join(",") : region;
url.searchParams.append("filter[region__in]", regionValue);
}
try {
const compliances = await fetch(url.toString(), {
headers: {
Accept: "application/vnd.api+json",
Authorization: `Bearer ${session?.accessToken}`,
},
});
const data = await compliances.json();
const parsedData = parseStringify(data);
revalidatePath("/compliance");
return parsedData;
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching providers:", error);
return undefined;
}
};
+1
View File
@@ -0,0 +1 @@
export * from "./compliances";
+1
View File
@@ -0,0 +1 @@
export * from "./invitation";
+4 -2
View File
@@ -42,6 +42,7 @@ export const getInvitations = async ({
revalidatePath("/invitations");
return parsedData;
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching invitations:", error);
return undefined;
}
@@ -56,7 +57,7 @@ export const sendInvite = async (formData: FormData) => {
const body = JSON.stringify({
data: {
type: "Invitation",
type: "invitations",
attributes: {
email,
},
@@ -104,7 +105,7 @@ export const updateInvite = async (formData: FormData) => {
},
body: JSON.stringify({
data: {
type: "Invitation",
type: "invitations",
id: invitationId,
attributes: {
email: invitationEmail,
@@ -117,6 +118,7 @@ export const updateInvite = async (formData: FormData) => {
revalidatePath("/invitations");
return parseStringify(data);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error updating invitation:", error);
return {
error: getErrorMessage(error),
+4 -4
View File
@@ -90,7 +90,7 @@ export const updateProvider = async (formData: FormData) => {
},
body: JSON.stringify({
data: {
type: "Provider",
type: "providers",
id: providerId,
attributes: {
alias: providerAlias,
@@ -129,7 +129,7 @@ export const addProvider = async (formData: FormData) => {
},
body: JSON.stringify({
data: {
type: "Provider",
type: "providers",
attributes: {
provider: providerType,
uid: providerUid,
@@ -209,7 +209,7 @@ export const addCredentialsProvider = async (formData: FormData) => {
const bodyData = {
data: {
type: "ProviderSecret",
type: "provider-secrets",
attributes: {
secret_type: secretType,
secret,
@@ -219,7 +219,7 @@ export const addCredentialsProvider = async (formData: FormData) => {
provider: {
data: {
id: providerId,
type: "Provider",
type: "providers",
},
},
},
+3 -3
View File
@@ -91,14 +91,14 @@ export const scanOnDemand = async (formData: FormData) => {
},
body: JSON.stringify({
data: {
type: "Scan",
type: "scans",
attributes: {
name: scanName,
},
relationships: {
provider: {
data: {
type: "Provider",
type: "providers",
id: providerId,
},
},
@@ -136,7 +136,7 @@ export const updateScan = async (formData: FormData) => {
},
body: JSON.stringify({
data: {
type: "Scan",
type: "scans",
id: scanId,
attributes: {
name: scanName,
+1 -1
View File
@@ -69,7 +69,7 @@ export const updateUser = async (formData: FormData) => {
},
body: JSON.stringify({
data: {
type: "User",
type: "users",
id: userId,
attributes: {
name: userName,
+80 -27
View File
@@ -1,30 +1,55 @@
import { Spacer } from "@nextui-org/react";
import { redirect } from "next/navigation";
import { Suspense } from "react";
import { getCompliance } from "@/actions/compliance";
import { getCompliancesOverview } from "@/actions/compliances";
import { getScans } from "@/actions/scans";
import {
ComplianceCard,
ComplianceSkeletonGrid,
} from "@/components/compliance";
import { FilterControls } from "@/components/filters";
import { DataCompliance } from "@/components/compliance/data-compliance";
import { Header } from "@/components/ui";
import { SearchParamsProps } from "@/types";
import { ComplianceOverviewData, SearchParamsProps } from "@/types";
export default async function Compliance({
searchParams,
}: {
searchParams: SearchParamsProps;
}) {
const searchParamsKey = JSON.stringify(searchParams || {});
const scansData = await getScans({});
const scanList = scansData?.data.map((scan: any) => ({
id: scan.id,
name: scan.attributes.name || "Unnamed Scan",
state: scan.attributes.state,
progress: scan.attributes.progress,
}));
const selectedScanId = searchParams.scanId || scanList[0]?.id;
// Fetch compliance data for regions
const compliancesData = await getCompliancesOverview({
scanId: selectedScanId,
});
// Extract unique regions
const regions = compliancesData?.data
? Array.from(
new Set(
compliancesData.data.map(
(compliance: ComplianceOverviewData) =>
compliance.attributes.region as string,
),
),
)
: [];
return (
<>
<Header title="Compliance" icon="fluent-mdl2:compliance-audit" />
<Spacer y={4} />
<FilterControls mutedFindings={false} />
<Spacer y={4} />
<Suspense key={searchParamsKey} fallback={<ComplianceSkeletonGrid />}>
<DataCompliance scans={scanList} regions={regions as string[]} />
<Spacer y={12} />
<Suspense fallback={<ComplianceSkeletonGrid />}>
<SSRComplianceGrid searchParams={searchParams} />
</Suspense>
</>
@@ -36,28 +61,56 @@ const SSRComplianceGrid = async ({
}: {
searchParams: SearchParamsProps;
}) => {
const page = parseInt(searchParams.page?.toString() || "1", 10);
const compliancesData = await getCompliance({ page });
const [compliances] = await Promise.all([compliancesData]);
const scanId = searchParams.scanId?.toString() || "";
if (compliances?.errors) redirect("/compliance");
const regionFilter = searchParams["filter[region__in]"]?.toString() || "";
// Fetch compliance data
const compliancesData = await getCompliancesOverview({
scanId,
region: regionFilter,
});
// Check if the response contains no data
if (!compliancesData || compliancesData?.data?.length === 0) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-default-500">
No compliance data available for the selected scan.
</div>
</div>
);
}
// Handle errors returned by the API
if (compliancesData?.errors?.length > 0) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-default-500">Provide a valid scan ID.</div>
</div>
);
}
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4">
{compliances.compliance?.data.map((compliance: any) => (
<ComplianceCard
key={compliance.id}
title={compliance.attributes.title}
passingRequirements={compliance.attributes.passingRequirements}
totalRequirements={compliance.attributes.totalRequirements}
prevPassingRequirements={
compliance.lastScan.attributes.passingRequirements
}
prevTotalRequirements={
compliance.lastScan.attributes.totalRequirements
}
/>
))}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{compliancesData.data.map((compliance: ComplianceOverviewData) => {
const { attributes } = compliance;
const {
framework,
requirements_status: { passed, total },
} = attributes;
return (
<ComplianceCard
key={compliance.id}
title={framework}
passingRequirements={passed}
totalRequirements={total}
prevPassingRequirements={passed}
prevTotalRequirements={total}
/>
);
})}
</div>
);
};
-10
View File
@@ -1,10 +0,0 @@
import { NextResponse } from "next/server";
import data from "../../../dataCompliance.json";
export async function GET() {
// Simulate fetching data with a delay
await new Promise((resolve) => setTimeout(resolve, 2000));
return NextResponse.json({ compliance: data });
}
+1 -1
View File
@@ -16,7 +16,7 @@ const refreshAccessToken = async (token: JwtPayload) => {
const bodyData = {
data: {
type: "TokenRefresh",
type: "tokens-refresh",
attributes: {
refresh: (token as any).refreshToken,
},
@@ -0,0 +1,90 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { SelectScanComplianceData } from "@/components/compliance/data-compliance";
import { CrossIcon } from "@/components/icons";
import { CustomButton, CustomDropdownFilter } from "@/components/ui/custom";
interface DataComplianceProps {
scans: { id: string; name: string; state: string; progress: number }[];
regions: string[];
}
export const DataCompliance = ({ scans, regions }: DataComplianceProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [showClearButton, setShowClearButton] = useState(false);
const scanIdParam = searchParams.get("scanId");
const selectedScanId = scanIdParam || scans[0]?.id;
useEffect(() => {
const hasFilters = Array.from(searchParams.keys()).some(
(key) => key.startsWith("filter[") || key === "sort",
);
setShowClearButton(hasFilters);
}, [searchParams]);
const handleScanChange = (selectedKey: string) => {
const params = new URLSearchParams(searchParams);
params.set("scanId", selectedKey);
router.push(`?${params.toString()}`);
};
const pushDropdownFilter = useCallback(
(key: string, values: string[]) => {
const params = new URLSearchParams(searchParams);
const filterKey = `filter[${key}]`;
if (values.length === 0) {
params.delete(filterKey);
} else {
params.set(filterKey, values.join(","));
}
router.push(`?${params.toString()}`);
},
[router, searchParams],
);
const clearAllFilters = useCallback(() => {
const params = new URLSearchParams(searchParams.toString());
Array.from(params.keys()).forEach((key) => {
if (key.startsWith("filter[") || key === "sort") {
params.delete(key);
}
});
router.push(`?${params.toString()}`, { scroll: false });
}, [router, searchParams]);
return (
<div className="flex flex-col gap-4">
<div className="grid grid-cols-1 items-center gap-x-4 gap-y-4 md:grid-cols-2 xl:grid-cols-4">
<SelectScanComplianceData
scans={scans}
selectedScanId={selectedScanId}
onSelectionChange={handleScanChange}
/>
<CustomDropdownFilter
filter={{
key: "region__in",
values: regions,
labelCheckboxGroup: "Regions",
}}
onFilterChange={pushDropdownFilter}
/>
{showClearButton && (
<CustomButton
ariaLabel="Reset"
className="w-full md:w-fit"
onPress={clearAllFilters}
variant="dashed"
size="sm"
endContent={<CrossIcon size={24} />}
radius="sm"
>
Reset
</CustomButton>
)}
</div>
</div>
);
};
@@ -0,0 +1,2 @@
export * from "./data-compliance";
export * from "./select-scan-compliance-data";
@@ -0,0 +1,50 @@
import { Select, SelectItem } from "@nextui-org/react";
interface SelectScanComplianceDataProps {
scans: { id: string; name: string; state: string; progress: number }[];
selectedScanId: string;
onSelectionChange: (selectedKey: string) => void;
}
export const SelectScanComplianceData = ({
scans,
selectedScanId,
onSelectionChange,
}: SelectScanComplianceDataProps) => {
return (
<Select
aria-label="Select a Scan"
placeholder="Select a scan"
labelPlacement="outside"
size="md"
selectedKeys={new Set([selectedScanId])}
onSelectionChange={(keys) =>
onSelectionChange(Array.from(keys)[0] as string)
}
renderValue={() => {
const selectedItem = scans.find((item) => item.id === selectedScanId);
return selectedItem ? (
<div className="flex flex-col">
<span className="font-bold">{selectedItem.name}</span>
<span className="text-sm text-gray-500">
State: {selectedItem.state}, Progress: {selectedItem.progress}%
</span>
</div>
) : (
"Select a scan"
);
}}
>
{scans.map((scan) => (
<SelectItem key={scan.id} textValue={scan.name}>
<div className="flex flex-col">
<span className="font-bold">{scan.name}</span>
<span className="text-sm text-gray-500">
State: {scan.state}, Progress: {scan.progress}%
</span>
</div>
</SelectItem>
))}
</Select>
);
};
+2 -2
View File
@@ -1,2 +1,2 @@
export * from "./ComplianceCard";
export * from "./ComplianceSkeletonGrid";
export * from "./compliance-card";
export * from "./compliance-skeleton-grid";
-438
View File
@@ -1,438 +0,0 @@
{
"links": {
"first": "http://localhost:8080/api/v1/compliance?page%5Bnumber%5D=1",
"last": "http://localhost:8080/api/v1/compliance?page%5Bnumber%5D=1",
"next": null,
"prev": null
},
"data": [
{
"id": "001",
"attributes": {
"title": "AWS Account Security Onboarding",
"passingRequirements": 28,
"totalRequirements": 83,
"regions": ["us-east-1", "us-east-2"]
},
"lastScan": {
"attributes": {
"passingRequirements": 28,
"totalRequirements": 83
}
}
},
{
"id": "002",
"attributes": {
"title": "AWS Audit Manager Control Tower Guardrails",
"passingRequirements": 10,
"totalRequirements": 14,
"regions": ["us-west-1", "us-west-2"]
},
"lastScan": {
"attributes": {
"passingRequirements": 14,
"totalRequirements": 14
}
}
},
{
"id": "003",
"attributes": {
"title": "AWS Foundational Security Best Practices",
"passingRequirements": 22,
"totalRequirements": 37,
"regions": ["eu-west-1", "eu-west-2"]
},
"lastScan": {
"attributes": {
"passingRequirements": 22,
"totalRequirements": 37
}
}
},
{
"id": "004",
"attributes": {
"title": "AWS Foundational Technical Review",
"passingRequirements": 3,
"totalRequirements": 45,
"regions": ["ap-southeast-1", "ap-southeast-2"]
},
"lastScan": {
"attributes": {
"passingRequirements": 25,
"totalRequirements": 45
}
}
},
{
"id": "005",
"attributes": {
"title": "AWS Well Architected Framework Reliability Pillar",
"passingRequirements": 2,
"totalRequirements": 3,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 0,
"totalRequirements": 3
}
}
},
{
"id": "006",
"attributes": {
"title": "AWS Well Architected Framework Security Pillar",
"passingRequirements": 19,
"totalRequirements": 57,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 24,
"totalRequirements": 57
}
}
},
{
"id": "007",
"attributes": {
"title": "CIS 1.4",
"passingRequirements": 43,
"totalRequirements": 58,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 40,
"totalRequirements": 58
}
}
},
{
"id": "008",
"attributes": {
"title": "CIS 1.5",
"passingRequirements": 48,
"totalRequirements": 63,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 50,
"totalRequirements": 63
}
}
},
{
"id": "009",
"attributes": {
"title": "CIS 2.0",
"passingRequirements": 47,
"totalRequirements": 64,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 64,
"totalRequirements": 64
}
}
},
{
"id": "010",
"attributes": {
"title": "CIS 3.0",
"passingRequirements": 45,
"totalRequirements": 62,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 48,
"totalRequirements": 62
}
}
},
{
"id": "011",
"attributes": {
"title": "CISA",
"passingRequirements": 4,
"totalRequirements": 16,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 4,
"totalRequirements": 16
}
}
},
{
"id": "012",
"attributes": {
"title": "ENS RD2022 Categoría ALTA",
"passingRequirements": 89,
"totalRequirements": 189,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 82,
"totalRequirements": 189
}
}
},
{
"id": "013",
"attributes": {
"title": "FFIEC",
"passingRequirements": 8,
"totalRequirements": 44,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 40,
"totalRequirements": 44
}
}
},
{
"id": "014",
"attributes": {
"title": "FedRAMP Low Revision 4",
"passingRequirements": 4,
"totalRequirements": 18,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 4,
"totalRequirements": 18
}
}
},
{
"id": "015",
"attributes": {
"title": "FedRamp Moderate Revision 4",
"passingRequirements": 11,
"totalRequirements": 64,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 28,
"totalRequirements": 64
}
}
},
{
"id": "016",
"attributes": {
"title": "GDPR",
"passingRequirements": 0,
"totalRequirements": 3,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 0,
"totalRequirements": 3
}
}
},
{
"id": "017",
"attributes": {
"title": "GxP 21 CFR Part 11",
"passingRequirements": 1,
"totalRequirements": 11,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 11,
"totalRequirements": 11
}
}
},
{
"id": "018",
"attributes": {
"title": "GxP EU Annex 11",
"passingRequirements": 5,
"totalRequirements": 14,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 5,
"totalRequirements": 14
}
}
},
{
"id": "019",
"attributes": {
"title": "HIPAA",
"passingRequirements": 2,
"totalRequirements": 32,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 9,
"totalRequirements": 32
}
}
},
{
"id": "020",
"attributes": {
"title": "ISO27001 2013",
"passingRequirements": 55,
"totalRequirements": 79,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 66,
"totalRequirements": 79
}
}
},
{
"id": "021",
"attributes": {
"title": "MITRE ATTACK",
"passingRequirements": 7,
"totalRequirements": 46,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 19,
"totalRequirements": 46
}
}
},
{
"id": "022",
"attributes": {
"title": "NIST 800 171 Revision 2",
"passingRequirements": 3,
"totalRequirements": 50,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 3,
"totalRequirements": 50
}
}
},
{
"id": "023",
"attributes": {
"title": "NIST 800 53 Revision 4",
"passingRequirements": 21,
"totalRequirements": 64,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 21,
"totalRequirements": 64
}
}
},
{
"id": "024",
"attributes": {
"title": "NIST 800 53 Revision 5",
"passingRequirements": 75,
"totalRequirements": 288,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 75,
"totalRequirements": 288
}
}
},
{
"id": "025",
"attributes": {
"title": "NIST CSF 1.1",
"passingRequirements": 15,
"totalRequirements": 56,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 46,
"totalRequirements": 56
}
}
},
{
"id": "026",
"attributes": {
"title": "PCI 3.2.1",
"passingRequirements": 11,
"totalRequirements": 19,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 19,
"totalRequirements": 19
}
}
},
{
"id": "027",
"attributes": {
"title": "RBI Cyber Security Framework",
"passingRequirements": 3,
"totalRequirements": 9,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 3,
"totalRequirements": 9
}
}
},
{
"id": "028",
"attributes": {
"title": "SOC2",
"passingRequirements": 7,
"totalRequirements": 56,
"regions": []
},
"lastScan": {
"attributes": {
"passingRequirements": 7,
"totalRequirements": 56
}
}
}
],
"meta": {
"pagination": {
"page": 1,
"pages": 1,
"count": 28
},
"version": "v1"
}
}
+62 -15
View File
@@ -100,9 +100,56 @@ export interface ApiError {
};
code: string;
}
export interface CompliancesOverview {
links: {
first: string;
last: string;
next: string | null;
prev: string | null;
};
data: ComplianceOverviewData[];
meta: {
pagination: {
page: number;
pages: number;
count: number;
};
version: string;
};
}
export interface ComplianceOverviewData {
type: "compliance-overviews";
id: string;
attributes: {
inserted_at: string;
compliance_id: string;
framework: string;
version: string;
requirements_status: {
passed: number;
failed: number;
manual: number;
total: number;
};
region: string;
provider_type: string;
};
relationships: {
scan: {
data: {
type: "scans";
id: string;
};
};
};
links: {
self: string;
};
}
export interface InvitationProps {
type: "Invitation";
type: "invitations";
id: string;
attributes: {
inserted_at: string;
@@ -115,7 +162,7 @@ export interface InvitationProps {
relationships: {
inviter: {
data: {
type: "User";
type: "users";
id: string;
};
};
@@ -126,7 +173,7 @@ export interface InvitationProps {
}
export interface UserProps {
type: "User";
type: "users";
id: string;
attributes: {
name: string;
@@ -140,7 +187,7 @@ export interface UserProps {
count: number;
};
data: Array<{
type: "Membership";
type: "memberships";
id: string;
}>;
};
@@ -175,7 +222,7 @@ export interface ProviderProps {
}
export interface ScanProps {
type: "Scan";
type: "scans";
id: string;
attributes: {
name: string;
@@ -203,20 +250,20 @@ export interface ScanProps {
provider: {
data: {
id: string;
type: "Provider";
type: "providers";
};
};
task: {
data: {
id: string;
type: "Task";
type: "tasks";
};
};
};
}
export interface FindingProps {
type: "Finding";
type: "findings";
id: string;
attributes: {
uid: string;
@@ -264,13 +311,13 @@ export interface FindingProps {
relationships: {
resources: {
data: {
type: "Resource";
type: "resources";
id: string;
}[];
};
scan: {
data: {
type: "Scan";
type: "scans";
id: string;
};
attributes: {
@@ -290,7 +337,7 @@ export interface FindingProps {
};
resource: {
data: {
type: "Resource";
type: "resources";
id: string;
}[];
id: string;
@@ -307,7 +354,7 @@ export interface FindingProps {
relationships: {
provider: {
data: {
type: "Provider";
type: "providers";
id: string;
};
};
@@ -316,7 +363,7 @@ export interface FindingProps {
count: number;
};
data: {
type: "Finding";
type: "findings";
id: string;
}[];
};
@@ -327,7 +374,7 @@ export interface FindingProps {
};
provider: {
data: {
type: "Provider";
type: "providers";
id: string;
};
attributes: {
@@ -344,7 +391,7 @@ export interface FindingProps {
relationships: {
secret: {
data: {
type: "ProviderSecret";
type: "provider-secrets";
id: string;
};
};