diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 04f90c81d7..6f1426da12 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -25,6 +25,7 @@ module.exports = {
eqeqeq: 2,
quotes: ["error", "double", "avoid-escape"],
"@typescript-eslint/no-explicit-any": "off",
+ "security/detect-object-injection": "off",
"prettier/prettier": [
"error",
{
diff --git a/actions/findings/findings.ts b/actions/findings/findings.ts
new file mode 100644
index 0000000000..9fc89b0907
--- /dev/null
+++ b/actions/findings/findings.ts
@@ -0,0 +1,49 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { redirect } from "next/navigation";
+
+import { auth } from "@/auth.config";
+import { parseStringify } from "@/lib";
+
+export const getFindings = async ({
+ page = 1,
+ query = "",
+ sort = "",
+ filters = {},
+}) => {
+ const session = await auth();
+
+ if (isNaN(Number(page)) || page < 1)
+ redirect("findings?include=resources.provider,scan");
+
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/findings?include=resources.provider,scan`);
+
+ if (page) url.searchParams.append("page[number]", page.toString());
+ if (query) url.searchParams.append("filter[search]", query);
+ if (sort) url.searchParams.append("sort", sort);
+
+ // Handle multiple filters
+ Object.entries(filters).forEach(([key, value]) => {
+ if (key !== "filter[search]") {
+ url.searchParams.append(key, String(value));
+ }
+ });
+
+ try {
+ const findings = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await findings.json();
+ const parsedData = parseStringify(data);
+ revalidatePath("/findings");
+ return parsedData;
+ } catch (error) {
+ console.error("Error fetching findings:", error);
+ return undefined;
+ }
+};
diff --git a/actions/findings/index.ts b/actions/findings/index.ts
new file mode 100644
index 0000000000..eb3a674c67
--- /dev/null
+++ b/actions/findings/index.ts
@@ -0,0 +1 @@
+export * from "./findings";
diff --git a/app/(prowler)/findings/page.tsx b/app/(prowler)/findings/page.tsx
index 280ecf293b..f75638f792 100644
--- a/app/(prowler)/findings/page.tsx
+++ b/app/(prowler)/findings/page.tsx
@@ -1,426 +1,85 @@
import { Spacer } from "@nextui-org/react";
import React, { Suspense } from "react";
+import { getFindings } from "@/actions/findings";
+import { filterFindings } from "@/components/filters/data-filters";
+import { FilterControls } from "@/components/filters/filter-controls";
import {
- ColumnsFindings,
- DataTableFindings,
+ ColumnFindings,
SkeletonTableFindings,
-} from "@/components/findings";
+} from "@/components/findings/table";
import { Header } from "@/components/ui";
+import { DataTable } from "@/components/ui/table";
+import { createDict } from "@/lib";
+import { FindingProps, SearchParamsProps } from "@/types/components";
-export default async function Findings() {
+export default async function Findings({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) {
return (
<>
-
-
- }>
-
-
-
+
+
+
+ }>
+
+
>
);
}
-const SSRDataTable = async () => {
+const SSRDataTable = async ({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) => {
+ const page = parseInt(searchParams.page?.toString() || "1", 10);
+ const sort = searchParams.sort?.toString();
+
+ // Extract all filter parameters
+ const filters = Object.fromEntries(
+ Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")),
+ );
+
+ // Extract query from filters
+ const query = (filters["filter[search]"] as string) || "";
+
+ const findingsData = await getFindings({ query, page, sort, filters });
+
+ // Create dictionaries for resources, scans, and providers
+ const resourceDict = createDict("Resource", findingsData);
+ const scanDict = createDict("Scan", findingsData);
+ const providerDict = createDict("Provider", findingsData);
+
+ // Expand each finding with its corresponding resource, scan, and provider
+ const expandedFindings = findingsData.data.map((finding: FindingProps) => {
+ const scan = scanDict[finding.relationships?.scan?.data?.id];
+ const resource =
+ resourceDict[finding.relationships?.resources?.data?.[0]?.id];
+ const provider = providerDict[resource?.relationships?.provider?.data?.id];
+
+ return {
+ ...finding,
+ relationships: { scan, resource, provider },
+ };
+ });
+
+ // Create the new object while maintaining the original structure
+ const expandedResponse = {
+ ...findingsData,
+ data: expandedFindings,
+ };
+
return (
- ",
- resourceLink:
- "https://app.prowler.pro/app/findings?search=%3Croot_account%3E",
- resourceARN: "arn:aws:iam::714274078102:root",
- checkId: "iam_root_mfa_enabled",
- checkLink:
- "https://app.prowler.pro/app/findings?search=iam_root_mfa_enabled",
- type: [
- "Software and Configuration Checks",
- "Industry and Regulatory Standards",
- "CIS AWS Foundations Benchmark",
- ],
- scanTime: "2024-07-17T09:55:14.191475Z",
- findingId: "bc3a34e0-16f0-4ea1-ac62-f796c8af3448",
- findingLink:
- "https://app.prowler.pro/app/findings?date=2024-08-05&search=bc3a34e0-16f0-4ea1-ac62-f796c8af3448",
- details: "MFA is not enabled for root account.",
- riskLink: "",
- riskDetails:
- "The root account is the most privileged user in an AWS account. MFA adds an extra layer of protection on top of a user name and password. With MFA enabled when a user signs in to an AWS website they will be prompted for their user name and password as well as for an authentication code from their AWS MFA device. When virtual MFA is used for root accounts it is recommended that the device used is NOT a personal device but rather a dedicated mobile device (tablet or phone) that is managed to be kept charged and secured independent of any individual personal devices. (non-personal virtual MFA) This lessens the risks of losing access to the MFA due to device loss / trade-in or if the individual owning the device is no longer employed at the company.",
- recommendationLink:
- "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html#id_root-user_manage_mfa",
- recommendationDetails:
- "Using IAM console navigate to Dashboard and expand Activate MFA on your root account.",
- referenceInformation: "",
- referenceLink: "",
- },
- },
- {
- id: "67890",
- attributes: {
- CheckTitle:
- "Ensure S3 buckets with public read/write access are not allowed",
- severity: "high",
- status: "fail",
- region: "us-east-1",
- service: "s3",
- account: "prod (987654321012)",
- },
- card: {
- resourceId: "bucket-example-public-read-write",
- resourceLink:
- "https://app.prowler.pro/app/findings?date=2024-08-06&search=bucket-example-public-read-write",
- resourceARN: "arn:aws:s3:::bucket-example-public-read-write",
- checkId: "s3_bucket_public_access",
- checkLink:
- "https://app.prowler.pro/app/findings?date=2024-08-06&search=s3_bucket_public_access",
- type: ["Security"],
- scanTime: "2024-07-17T09:55:14.191475Z",
- findingId: "e7b3d6a2-39a1-4f0e-9b78-29f4e6f292d1",
- findingLink:
- "https://app.prowler.pro/app/findings?date=2024-08-06&search=e7b3d6a2-39a1-4f0e-9b78-29f4e6f292d1",
- details:
- "S3 bucket example-public-read-write allows public read/write access.",
- riskLink:
- "https://docs.aws.amazon.com/AmazonS3/latest/dev/security-best-practices.html",
- riskDetails:
- "Publicly accessible S3 buckets can expose sensitive data and be exploited by attackers.",
- recommendationLink:
- "https://docs.aws.amazon.com/AmazonS3/latest/user-guide/block-public-access.html",
- recommendationDetails:
- "Use S3 Block Public Access to prevent public access to your S3 buckets.",
- referenceInformation: "AWS Console",
- referenceLink:
- "https://docs.prowler.com/checks/aws/s3-policies/bc_aws_s3_1/#console",
- },
- },
- {
- id: "11223",
- attributes: {
- CheckTitle:
- "Ensure IAM password policy requires minimum length of 12 characters",
- severity: "medium",
- status: "fail",
- region: "eu-central-1",
- service: "iam",
- account: "staging (123456789012)",
- },
- card: {
- resourceId: "password-policy",
- resourceLink:
- "https://app.prowler.pro/app/findings?date=2024-08-06&search=password-policy",
- resourceARN: "arn:aws:iam::123456789012:password-policy",
- checkId: "iam_password_policy_min_length",
- checkLink:
- "https://app.prowler.pro/app/findings?date=2024-08-06&search=iam_password_policy_min_length",
- type: ["Security"],
- scanTime: "2024-07-17T09:55:14.191475Z",
- findingId: "c2b3d1a4-7a9f-4d5c-a9ef-765a1d7f421c",
- findingLink:
- "https://app.prowler.pro/app/findings?date=2024-08-06&search=c2b3d1a4-7a9f-4d5c-a9ef-765a1d7f421c",
- details:
- "IAM password policy does not require a minimum length of 12 characters.",
- riskLink:
- "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html",
- riskDetails:
- "Weak password policies increase the risk of unauthorized access to AWS resources.",
- recommendationLink:
- "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html",
- recommendationDetails:
- "Enforce a minimum password length of 12 characters in your IAM password policy.",
- referenceInformation: "AWS CLI",
- referenceLink:
- "https://docs.prowler.com/checks/aws/iam-policies/bc_aws_iam_1/#cli-command",
- },
- },
- {
- id: "44556",
- attributes: {
- CheckTitle: "Ensure RDS instances are not publicly accessible",
- severity: "high",
- status: "muted",
- region: "ap-southeast-1",
- service: "rds",
- account: "prod (234567890123)",
- },
- card: {
- resourceId: "rds-instance-public-access",
- resourceLink:
- "https://app.prowler.pro/app/findings?date=2024-08-06&search=rds-instance-public-access",
- resourceARN:
- "arn:aws:rds:ap-southeast-1:234567890123:db:rds-instance-public-access",
- checkId: "rds_instance_public_access",
- checkLink:
- "https://app.prowler.pro/app/findings?date=2024-08-06&search=rds_instance_public_access",
- type: ["Security"],
- scanTime: "2024-07-17T09:55:14.191475Z",
- findingId: "f3b4c5d6-19a7-45d8-bc3e-8c5f6a7d8e9b",
- findingLink:
- "https://app.prowler.pro/app/findings?date=2024-08-06&search=f3b4c5d6-19a7-45d8-bc3e-8c5f6a7d8e9b",
- details:
- "RDS instance is not publicly accessible, which adheres to security best practices.",
- riskLink:
- "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.RDSSecurity.html",
- riskDetails:
- "Publicly accessible RDS instances can be attacked by unauthorized users.",
- recommendationLink:
- "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.Scenarios.html",
- recommendationDetails:
- "Ensure RDS instances are not publicly accessible by placing them in private subnets.",
- referenceInformation: "AWS Console",
- referenceLink:
- "https://docs.prowler.com/checks/aws/rds-policies/bc_aws_rds_1/#console",
- },
- },
- {
- id: "77889",
- attributes: {
- CheckTitle: "Ensure EBS volumes are encrypted",
- severity: "critical",
- status: "fail",
- region: "eu-west-1",
- service: "ec2",
- account: "prod (345678901234)",
- },
- card: {
- resourceId: "volume-0123456789abcdef0",
- resourceLink:
- "https://app.prowler.pro/app/findings?date=2024-08-07&search=volume-0123456789abcdef0",
- resourceARN:
- "arn:aws:ec2:eu-west-1:345678901234:volume/volume-0123456789abcdef0",
- checkId: "ebs_volume_encryption",
- checkLink:
- "https://app.prowler.pro/app/findings?date=2024-08-07&search=ebs_volume_encryption",
- type: ["Encryption"],
- scanTime: "2024-07-17T09:55:14.191475Z",
- findingId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
- findingLink:
- "https://app.prowler.pro/app/findings?date=2024-08-07&search=a1b2c3d4-e5f6-7890-abcd-ef1234567890",
- details: "EBS volume volume-0123456789abcdef0 is not encrypted.",
- riskLink:
- "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html",
- riskDetails:
- "Unencrypted EBS volumes can expose sensitive data if compromised.",
- recommendationLink:
- "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html",
- recommendationDetails:
- "Enable encryption for EBS volumes using AWS-managed or customer-managed keys.",
- referenceInformation: "AWS CLI",
- referenceLink:
- "https://docs.prowler.com/checks/aws/ec2-policies/bc_aws_ec2_1/#cli-command",
- },
- },
- {
- id: "99100",
- attributes: {
- CheckTitle: "Ensure CloudTrail is enabled in all regions",
- severity: "critical",
- status: "fail",
- region: "us-west-2",
- service: "cloudtrail",
- account: "dev (456789012345)",
- },
- card: {
- resourceId: "cloudtrail-all-regions",
- resourceLink:
- "https://app.prowler.pro/app/findings?date=2024-08-07&search=cloudtrail-all-regions",
- resourceARN:
- "arn:aws:cloudtrail:us-west-2:456789012345:trail/cloudtrail-all-regions",
- checkId: "cloudtrail_all_regions_enabled",
- checkLink:
- "https://app.prowler.pro/app/findings?date=2024-08-07&search=cloudtrail_all_regions_enabled",
- type: ["Logging"],
- scanTime: "2024-07-17T09:55:14.191475Z",
- findingId: "b2c3d4e5-f6a7-8901-bcde-f123456789ab",
- findingLink:
- "https://app.prowler.pro/app/findings?date=2024-08-07&search=b2c3d4e5-f6a7-8901-bcde-f123456789ab",
- details:
- "CloudTrail is not enabled for all regions in the account.",
- riskLink:
- "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-best-practices.html",
- riskDetails:
- "Without CloudTrail enabled in all regions, activities in non-monitored regions may go unnoticed.",
- recommendationLink:
- "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/creating-trail-using-console.html",
- recommendationDetails:
- "Enable CloudTrail across all regions to ensure comprehensive monitoring.",
- referenceInformation: "AWS Console",
- referenceLink:
- "https://docs.prowler.com/checks/aws/cloudtrail-policies/bc_aws_cloudtrail_1/#console",
- },
- },
- {
- id: "22334",
- attributes: {
- CheckTitle: "Ensure EC2 instances do not use outdated AMIs",
- severity: "medium",
- status: "fail",
- region: "us-east-1",
- service: "ec2",
- account: "prod (567890123456)",
- },
- card: {
- resourceId: "instance-ami-outdated",
- resourceLink:
- "https://app.prowler.pro/app/findings?date=2024-08-07&search=instance-ami-outdated",
- resourceARN:
- "arn:aws:ec2:us-east-1:567890123456:instance/instance-ami-outdated",
- checkId: "ec2_instance_outdated_ami",
- checkLink:
- "https://app.prowler.pro/app/findings?date=2024-08-07&search=ec2_instance_outdated_ami",
- type: ["Configuration"],
- scanTime: "2024-07-17T09:55:14.191475Z",
- findingId: "c3d4e5f6-a789-0123-bcde-f234567890ab",
- findingLink:
- "https://app.prowler.pro/app/findings?date=2024-08-07&search=c3d4e5f6-a789-0123-bcde-f234567890ab",
- details:
- "EC2 instance instance-ami-outdated is using an outdated AMI.",
- riskLink:
- "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html",
- riskDetails:
- "Outdated AMIs may have unpatched vulnerabilities that can be exploited.",
- recommendationLink:
- "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/creating-an-ami.html",
- recommendationDetails:
- "Update EC2 instances to use the latest AMIs with all security patches applied.",
- referenceInformation: "AWS CLI",
- referenceLink:
- "https://docs.prowler.com/checks/aws/ec2-policies/bc_aws_ec2_2/#cli-command",
- },
- },
- {
- id: "55667",
- attributes: {
- CheckTitle:
- "Ensure CloudWatch alarms exist for critical system events",
- severity: "high",
- status: "muted",
- region: "eu-west-2",
- service: "cloudwatch",
- account: "prod (678901234567)",
- },
- card: {
- resourceId: "cloudwatch-alarms",
- resourceLink:
- "https://app.prowler.pro/app/findings?date=2024-08-08&search=cloudwatch-alarms",
- resourceARN:
- "arn:aws:cloudwatch:eu-west-2:678901234567:alarm/cloudwatch-alarms",
- checkId: "cloudwatch_alarms_for_critical_events",
- checkLink:
- "https://app.prowler.pro/app/findings?date=2024-08-08&search=cloudwatch_alarms_for_critical_events",
- type: ["Monitoring"],
- scanTime: "2024-07-17T09:55:14.191475Z",
- findingId: "d4e5f6a7-8901-bcde-f345678901ab",
- findingLink:
- "https://app.prowler.pro/app/findings?date=2024-08-08&search=d4e5f6a7-8901-bcde-f345678901ab",
- details:
- "CloudWatch alarms are correctly configured for critical system events.",
- riskLink:
- "https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatchAlarms.html",
- riskDetails:
- "Without alarms for critical events, key issues may go unnoticed, leading to outages.",
- recommendationLink:
- "https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Create_Alarm.html",
- recommendationDetails:
- "Ensure alarms are set up for critical events to trigger timely notifications.",
- referenceInformation: "AWS Console",
- referenceLink:
- "https://docs.prowler.com/checks/aws/cloudwatch-policies/bc_aws_cloudwatch_1/#console",
- },
- },
- {
- id: "88900",
- attributes: {
- CheckTitle: "Ensure IAM users have least privilege permissions",
- severity: "medium",
- status: "fail",
- region: "ap-northeast-1",
- service: "iam",
- account: "dev (789012345678)",
- },
- card: {
- resourceId: "iam-user-least-privilege",
- resourceLink:
- "https://app.prowler.pro/app/findings?date=2024-08-08&search=iam-user-least-privilege",
- resourceARN:
- "arn:aws:iam::789012345678:user/iam-user-least-privilege",
- checkId: "iam_user_least_privilege",
- checkLink:
- "https://app.prowler.pro/app/findings?date=2024-08-08&search=iam_user_least_privilege",
- type: ["Security"],
- scanTime: "2024-07-17T09:55:14.191475Z",
- findingId: "e5f6a7b8-9012-bcde-f456789012ab",
- findingLink:
- "https://app.prowler.pro/app/findings?date=2024-08-08&search=e5f6a7b8-9012-bcde-f456789012ab",
- details:
- "IAM user iam-user-least-privilege has more permissions than necessary.",
- riskLink:
- "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html",
- riskDetails:
- "Excessive permissions increase the risk of privilege escalation and security breaches.",
- recommendationLink:
- "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html",
- recommendationDetails:
- "Apply the principle of least privilege to IAM users by restricting their permissions to the minimum necessary.",
- referenceInformation: "AWS CLI",
- referenceLink:
- "https://docs.prowler.com/checks/aws/iam-policies/bc_aws_iam_2/#cli-command",
- },
- },
- ]}
+
);
};
diff --git a/components/filters/data-filters.ts b/components/filters/data-filters.ts
index d6d876b522..2b2d319783 100644
--- a/components/filters/data-filters.ts
+++ b/components/filters/data-filters.ts
@@ -27,3 +27,22 @@ export const filterScans = [
},
// Add more filter categories as needed
];
+
+export const filterFindings = [
+ {
+ key: "severity",
+ labelCheckboxGroup: "Severity",
+ values: ["informational", "low", "medium", "high", "critical"],
+ },
+ {
+ key: "status",
+ labelCheckboxGroup: "Status",
+ values: ["PASS", "FAIL", "MANUAL", "MUTED"],
+ },
+ {
+ key: "delta",
+ labelCheckboxGroup: "Delta",
+ values: ["new", "changed"],
+ },
+ // Add more filter categories as needed
+];
diff --git a/components/findings/FindingsCard.tsx b/components/findings/FindingsCard.tsx
deleted file mode 100644
index b4f9cab7a6..0000000000
--- a/components/findings/FindingsCard.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { Divider } from "@nextui-org/react";
-import clsx from "clsx";
-import React from "react";
-
-import {
- FindingsCardContent,
- FindingsCardDetail,
- FindingsCardScan,
- FindingsCardType,
-} from "@/components/findings";
-import { FindingProps } from "@/types";
-interface FindingsCardProps {
- selectedRow: FindingProps;
-}
-
-export const FindingsCard: React.FC = ({ selectedRow }) => {
- const { attributes, card } = selectedRow || {};
- const { CheckTitle } = attributes || {};
- const {
- resourceLink,
- resourceId,
- resourceARN,
- checkLink,
- checkId,
- type,
- scanTime,
- findingLink,
- findingId,
- details,
- riskDetails,
- riskLink,
- recommendationDetails,
- recommendationLink,
- referenceInformation,
- referenceLink,
- } = card || {};
-
- return (
- <>
-
-
{CheckTitle}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
diff --git a/components/findings/FindingsCardContent.tsx b/components/findings/FindingsCardContent.tsx
deleted file mode 100644
index 09bba0150a..0000000000
--- a/components/findings/FindingsCardContent.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Link } from "@nextui-org/react";
-import React from "react";
-
-interface FindingsCardContentProps {
- title: string;
- url?: string;
- description: string;
-}
-
-export const FindingsCardContent: React.FC = ({
- title,
- url,
- description,
-}) => {
- return (
- <>
- {title}
- {url ? (
-
- {description}
-
- ) : (
- {description}
- )}
- >
- );
-};
diff --git a/components/findings/FindingsCardDetail.tsx b/components/findings/FindingsCardDetail.tsx
deleted file mode 100644
index 957cc6809e..0000000000
--- a/components/findings/FindingsCardDetail.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Button, Link } from "@nextui-org/react";
-import React from "react";
-
-interface FindingsCardDetailProps {
- title: string;
- url?: string;
- description: string;
- type?: DetailType;
-}
-
-type DetailType = "default" | "risk" | "recommendation" | "reference";
-
-const getDetailColorClass = (type: DetailType): string => {
- switch (type) {
- case "risk":
- return "border-red-200";
- case "recommendation":
- return "border-green-200";
- case "reference":
- return "border-gray-200";
- case "default":
- default:
- return "border-yellow-200";
- }
-};
-
-export const FindingsCardDetail: React.FC = ({
- title,
- url,
- description,
- type = "default",
-}) => {
- return (
- <>
- {description && (
-
-
- {title}
- {url && (
-
- )}
-
-
{description}
-
- )}
- >
- );
-};
diff --git a/components/findings/FindingsCardScan.tsx b/components/findings/FindingsCardScan.tsx
deleted file mode 100644
index 1ccab7a0a8..0000000000
--- a/components/findings/FindingsCardScan.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { format, parseISO } from "date-fns";
-import React from "react";
-
-interface FindingsCardScanProps {
- title: string;
- dateTime: string;
-}
-
-export const FindingsCardScan: React.FC = ({
- title,
- dateTime = "",
-}) => {
- const date = dateTime && parseISO(dateTime);
- const formattedDate = date && format(date, "MMM dd, yyyy");
- const formattedTime = date && format(date, "p 'UTC'");
-
- return (
- <>
- {title}
-
- {formattedDate} at {formattedTime}
-
- >
- );
-};
diff --git a/components/findings/FindingsCardType.tsx b/components/findings/FindingsCardType.tsx
deleted file mode 100644
index a525d0d1ab..0000000000
--- a/components/findings/FindingsCardType.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from "react";
-
-interface FindingsCardTypeProps {
- type: string[];
-}
-
-export const FindingsCardType: React.FC = ({
- type = [],
-}) => {
- const typeContent = () => {
- if (type.length > 0) {
- return type.join(", ");
- }
- return type[0];
- };
-
- return (
- <>
-
- {type.length > 1 ? "Types:" : "Type:"}
-
- {typeContent()}
- >
- );
-};
diff --git a/components/findings/index.ts b/components/findings/index.ts
deleted file mode 100644
index e7c1764519..0000000000
--- a/components/findings/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export * from "./FindingsCard";
-export * from "./FindingsCardContent";
-export * from "./FindingsCardDetail";
-export * from "./FindingsCardScan";
-export * from "./FindingsCardType";
-export * from "./table/ColumnsFindings";
-export * from "./table/DataTableFindings";
-export * from "./table/SkeletonTableFindings";
diff --git a/components/findings/table/ColumnsFindings.tsx b/components/findings/table/ColumnsFindings.tsx
deleted file mode 100644
index b27f4a6dbb..0000000000
--- a/components/findings/table/ColumnsFindings.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-"use client";
-
-import {
- Button,
- Dropdown,
- DropdownItem,
- DropdownMenu,
- DropdownTrigger,
-} from "@nextui-org/react";
-import { ColumnDef } from "@tanstack/react-table";
-
-import { VerticalDotsIcon } from "@/components/icons";
-import { SeverityBadge } from "@/components/ui/table";
-import { FindingProps } from "@/types";
-
-const getFindingsAttributes = (row: { original: FindingProps }) => {
- return row.original.attributes;
-};
-
-export const ColumnsFindings: ColumnDef[] = [
- {
- accessorKey: "checkTitle",
- header: "Check",
- cell: ({ row }) => {
- const { CheckTitle } = getFindingsAttributes(row);
- return {CheckTitle}
;
- },
- },
- {
- accessorKey: "severity",
- header: "Severity",
- cell: ({ row }) => {
- const { severity } = getFindingsAttributes(row);
- return ;
- },
- },
-
- {
- accessorKey: "region",
- header: "Region",
- cell: ({ row }) => {
- const { region } = getFindingsAttributes(row);
- return {region}
;
- },
- },
- {
- accessorKey: "service",
- header: "Service",
- cell: ({ row }) => {
- const { service } = getFindingsAttributes(row);
- return {service}
;
- },
- },
- {
- accessorKey: "account",
- header: "Account",
- cell: ({ row }) => {
- const { account } = getFindingsAttributes(row);
- return {account}
;
- },
- },
- {
- accessorKey: "actions",
- header: () => (
-
-
-
-
-
-
- Mute Findings for Selected Filters
- Configure Muted Findings
-
-
-
- ),
- },
-];
diff --git a/components/findings/table/DataTableFindings.tsx b/components/findings/table/DataTableFindings.tsx
deleted file mode 100644
index 1bc3884492..0000000000
--- a/components/findings/table/DataTableFindings.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-"use client";
-
-import { Button } from "@nextui-org/react";
-import {
- ColumnDef,
- flexRender,
- getCoreRowModel,
- getPaginationRowModel,
- Row,
- useReactTable,
-} from "@tanstack/react-table";
-import clsx from "clsx";
-
-import { FindingsCard } from "@/components/findings";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import { FindingProps } from "@/types";
-
-interface DataTableFindingsProps {
- columns: ColumnDef[];
- data: TData[];
-}
-
-export function DataTableFindings({
- columns,
- data,
-}: DataTableFindingsProps) {
- const table = useReactTable({
- data,
- columns,
- getCoreRowModel: getCoreRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- });
-
- const selectedRow = table.getSelectedRowModel().rows[0]?.original;
-
- const onClick = (row: Row) => {
- table.resetRowSelection(true);
- row.toggleSelected();
- return;
- };
-
- return (
- <>
-
-
-
-
-
- {table.getHeaderGroups().map((headerGroup) => (
-
- {headerGroup.headers.map((header) => {
- return (
-
- {header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext(),
- )}
-
- );
- })}
-
- ))}
-
-
- {table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row) => (
- onClick(row)}
- className={"hover:cursor-pointer"}
- >
- {row.getVisibleCells().map((cell) => (
-
- {flexRender(
- cell.column.columnDef.cell,
- cell.getContext(),
- )}
-
- ))}
-
- ))
- ) : (
-
-
- No results.
-
-
- )}
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/components/findings/table/column-findings.tsx b/components/findings/table/column-findings.tsx
new file mode 100644
index 0000000000..3391207093
--- /dev/null
+++ b/components/findings/table/column-findings.tsx
@@ -0,0 +1,148 @@
+"use client";
+
+import { ColumnDef } from "@tanstack/react-table";
+
+import {
+ DataTableColumnHeader,
+ SeverityBadge,
+ Status,
+ StatusBadge,
+} from "@/components/ui/table";
+import { FindingProps } from "@/types";
+
+import { DataTableRowActions } from "./data-table-row-actions";
+
+const statusMap: Record<"PASS" | "FAIL" | "MANUAL" | "MUTED", Status> = {
+ PASS: "completed",
+ FAIL: "failed",
+ MANUAL: "completed",
+ MUTED: "cancelled",
+};
+
+const getFindingsData = (row: { original: FindingProps }) => {
+ console.log(row.original);
+ return row.original;
+};
+
+const getFindingsMetadata = (row: { original: FindingProps }) => {
+ return row.original.attributes.check_metadata;
+};
+
+const getResourceData = (
+ row: { original: FindingProps },
+ field: keyof FindingProps["relationships"]["resource"]["attributes"],
+) => {
+ return (
+ row.original.relationships?.resource?.attributes?.[field] ||
+ `No ${field} found in resource`
+ );
+};
+
+const getProviderData = (
+ row: { original: FindingProps },
+ field: keyof FindingProps["relationships"]["provider"]["attributes"],
+) => {
+ return (
+ row.original.relationships?.provider?.attributes?.[field] ||
+ `No ${field} found in provider`
+ );
+};
+
+const getScanData = (
+ row: { original: FindingProps },
+ field: keyof FindingProps["relationships"]["scan"]["attributes"],
+) => {
+ return (
+ row.original.relationships?.scan?.attributes?.[field] ||
+ `No ${field} found in scan`
+ );
+};
+
+export const ColumnFindings: ColumnDef[] = [
+ {
+ accessorKey: "check",
+ header: "Check",
+ cell: ({ row }) => {
+ const { checktitle } = getFindingsMetadata(row);
+ return {checktitle}
;
+ },
+ },
+ {
+ accessorKey: "scanName",
+ header: "Scan Name",
+ cell: ({ row }) => {
+ const name = getScanData(row, "name");
+
+ return (
+
+ {typeof name === "string" || typeof name === "number"
+ ? name
+ : "Invalid data"}
+
+ );
+ },
+ },
+ {
+ accessorKey: "severity",
+ header: "Severity",
+ cell: ({ row }) => {
+ const {
+ attributes: { severity },
+ } = getFindingsData(row);
+ return ;
+ },
+ },
+ {
+ accessorKey: "status",
+ header: "Scan Status",
+ cell: ({ row }) => {
+ const {
+ attributes: { status },
+ } = getFindingsData(row);
+
+ const mappedStatus = statusMap[status];
+
+ return ;
+ },
+ },
+ {
+ accessorKey: "region",
+ header: "Region",
+ cell: ({ row }) => {
+ const region = getResourceData(row, "region");
+
+ return (
+ <>
+ {typeof region === "string" ? region : "Invalid region"}
+ >
+ );
+ },
+ },
+ {
+ accessorKey: "service",
+ header: "Service",
+ cell: ({ row }) => {
+ const { servicename } = getFindingsMetadata(row);
+ return {servicename}
;
+ },
+ },
+ {
+ accessorKey: "account",
+ header: "Account",
+ cell: ({ row }) => {
+ const account = getProviderData(row, "uid");
+
+ return (
+ <>
+ {typeof account === "string" ? account : "Invalid account"}
+ >
+ );
+ },
+ },
+ {
+ id: "actions",
+ cell: ({ row }) => {
+ return ;
+ },
+ },
+];
diff --git a/components/findings/table/data-table-row-actions.tsx b/components/findings/table/data-table-row-actions.tsx
new file mode 100644
index 0000000000..9513bb11a8
--- /dev/null
+++ b/components/findings/table/data-table-row-actions.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+import {
+ Button,
+ Dropdown,
+ DropdownItem,
+ DropdownMenu,
+ DropdownSection,
+ DropdownTrigger,
+} from "@nextui-org/react";
+import {
+ // AddNoteBulkIcon,
+ DeleteDocumentBulkIcon,
+ EditDocumentBulkIcon,
+} from "@nextui-org/shared-icons";
+import { Row } from "@tanstack/react-table";
+
+// import { useState } from "react";
+import { VerticalDotsIcon } from "@/components/icons";
+// import { CustomAlertModal } from "@/components/ui/custom";
+
+// import { EditForm } from "../forms";
+// import { DeleteForm } from "../forms/delete-form";
+
+interface DataTableRowActionsProps {
+ row: Row;
+}
+const iconClasses =
+ "text-2xl text-default-500 pointer-events-none flex-shrink-0";
+
+export function DataTableRowActions({
+ row,
+}: DataTableRowActionsProps) {
+ // const [isEditOpen, setIsEditOpen] = useState(false);
+ // const [isDeleteOpen, setIsDeleteOpen] = useState(false);
+ // const providerId = (row.original as { id: string }).id;
+ // const providerAlias = (row.original as any).attributes?.alias;
+ return (
+ <>
+ {/*
+
+
+
+
+ */}
+
+
+
+
+
+
+
+
+ }
+ // onClick={() => setIsEditOpen(true)}
+ >
+ Send to Jira
+
+ }
+ // onClick={() => setIsEditOpen(true)}
+ >
+ Send to Slack
+
+
+
+
+
+ >
+ );
+}
diff --git a/components/findings/table/index.ts b/components/findings/table/index.ts
new file mode 100644
index 0000000000..89d5150daf
--- /dev/null
+++ b/components/findings/table/index.ts
@@ -0,0 +1,3 @@
+export * from "./column-findings";
+export * from "./data-table-row-actions";
+export * from "./skeleton-table-findings";
diff --git a/components/findings/table/SkeletonTableFindings.tsx b/components/findings/table/skeleton-table-findings.tsx
similarity index 100%
rename from components/findings/table/SkeletonTableFindings.tsx
rename to components/findings/table/skeleton-table-findings.tsx
diff --git a/components/ui/table/severity-badge.tsx b/components/ui/table/severity-badge.tsx
index 9539e56bb3..8b852eea36 100644
--- a/components/ui/table/severity-badge.tsx
+++ b/components/ui/table/severity-badge.tsx
@@ -4,7 +4,7 @@ import React from "react";
import { AlertIcon } from "@/components/icons";
-type Severity = "critical" | "high" | "medium" | "low";
+type Severity = "informational" | "low" | "medium" | "high" | "critical";
const severityIconMap = {
critical: ,
diff --git a/components/ui/table/status-badge.tsx b/components/ui/table/status-badge.tsx
index c0f0356407..932d4105d6 100644
--- a/components/ui/table/status-badge.tsx
+++ b/components/ui/table/status-badge.tsx
@@ -1,7 +1,7 @@
import { Chip } from "@nextui-org/react";
import React from "react";
-type Status =
+export type Status =
| "available"
| "scheduled"
| "executing"
diff --git a/lib/custom.ts b/lib/helper.ts
similarity index 76%
rename from lib/custom.ts
rename to lib/helper.ts
index a33f07d6fe..7eeee17a6f 100644
--- a/lib/custom.ts
+++ b/lib/helper.ts
@@ -1,5 +1,17 @@
import { MetaDataProps } from "@/types";
+// Helper function to create dictionaries by type
+export const createDict = (
+ type: string,
+ data: any,
+ includedField: string = "included",
+) =>
+ Object.fromEntries(
+ data[includedField]
+ .filter((item: { type: string }) => item.type === type)
+ .map((item: { id: string }) => [item.id, item]),
+ );
+
export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value));
export const convertFileToUrl = (file: File) => URL.createObjectURL(file);
diff --git a/lib/index.ts b/lib/index.ts
index 38d384fb6e..56ebefcc21 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -1,2 +1,2 @@
-export * from "./custom";
+export * from "./helper";
export * from "./utils";
diff --git a/types/components.ts b/types/components.ts
index f88967e85e..086e6e4a60 100644
--- a/types/components.ts
+++ b/types/components.ts
@@ -108,32 +108,145 @@ export interface ScanProps {
}
export interface FindingProps {
+ type: "Finding";
id: string;
attributes: {
- CheckTitle: string;
- severity: "critical" | "high" | "medium" | "low";
- status: "fail" | "success" | "muted";
- region: string;
- service: string;
- account: string;
+ uid: string;
+ delta: "new" | "changed" | null;
+ status: "PASS" | "FAIL" | "MANUAL" | "MUTED";
+ status_extended: string;
+ severity: "informational" | "low" | "medium" | "high" | "critical";
+ check_id: string;
+ check_metadata: {
+ risk: string;
+ notes: string;
+ checkid: string;
+ provider: string;
+ severity: "informational" | "low" | "medium" | "high" | "critical";
+ checktype: string[];
+ dependson: string[];
+ relatedto: string[];
+ categories: string[];
+ checktitle: string;
+ compliance: string | null;
+ relatedurl: string;
+ description: string;
+ remediation: {
+ code: {
+ cli: string;
+ other: string;
+ nativeiac: string;
+ terraform: string;
+ };
+ recommendation: {
+ url: string;
+ text: string;
+ };
+ };
+ servicename: string;
+ checkaliases: string[];
+ resourcetype: string;
+ subservicename: string;
+ resourceidtemplate: string;
+ };
+ raw_result: object | null;
+ inserted_at: string;
+ updated_at: string;
};
- card: {
- resourceId: string;
- resourceLink: string;
- resourceARN: string;
- checkId: string;
- checkLink: string;
- type: string[];
- scanTime: string;
- findingId: string;
- findingLink: string;
- details: string;
- riskLink: string;
- riskDetails: string;
- recommendationLink: string;
- recommendationDetails: string;
- referenceInformation: string;
- referenceLink: string;
+ relationships: {
+ resources: {
+ data: {
+ type: "Resource";
+ id: string;
+ }[];
+ };
+ scan: {
+ data: {
+ type: "Scan";
+ id: string;
+ };
+ attributes: {
+ name: string;
+ trigger: string;
+ state: string;
+ unique_resource_count: number;
+ progress: number;
+ scanner_args: {
+ checks_to_execute: string[];
+ };
+ duration: number;
+ started_at: string;
+ completed_at: string;
+ scheduled_at: string | null;
+ };
+ };
+ resource: {
+ data: {
+ type: "Resource";
+ id: string;
+ }[];
+ attributes: {
+ uid: string;
+ name: string;
+ region: string;
+ service: string;
+ tags: Record;
+ type: string;
+ inserted_at: string;
+ updated_at: string;
+ };
+ relationships: {
+ provider: {
+ data: {
+ type: "Provider";
+ id: string;
+ };
+ };
+ findings: {
+ meta: {
+ count: number;
+ };
+ data: {
+ type: "Finding";
+ id: string;
+ }[];
+ };
+ };
+ links: {
+ self: string;
+ };
+ };
+ provider: {
+ data: {
+ type: "Provider";
+ id: string;
+ };
+ attributes: {
+ provider: string;
+ uid: string;
+ alias: string;
+ connection: {
+ connected: boolean;
+ last_checked_at: string;
+ };
+ inserted_at: string;
+ updated_at: string;
+ };
+ relationships: {
+ secret: {
+ data: {
+ type: "ProviderSecret";
+ id: string;
+ };
+ };
+ };
+ links: {
+ self: string;
+ };
+ };
+ };
+ links: {
+ self: string;
};
}