Merge pull request #75 from prowler-cloud/PRWLR-4982-Create-a-finding-component-integrate-with-API

Prwlr 4982 create a finding component integrate with api
This commit is contained in:
Pablo Lara
2024-10-22 11:09:33 +02:00
committed by GitHub
22 changed files with 537 additions and 883 deletions
+1
View File
@@ -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",
{
+49
View File
@@ -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;
}
};
+1
View File
@@ -0,0 +1 @@
export * from "./findings";
+66 -407
View File
@@ -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 (
<>
<Header title="Findings" icon="ph:list-checks-duotone" />
<Spacer />
<div className="flex w-full flex-col items-start overflow-hidden">
<Spacer y={6} />
<Suspense fallback={<SkeletonTableFindings />}>
<SSRDataTable />
</Suspense>
</div>
<Spacer y={4} />
<FilterControls search providers date />
<Spacer y={4} />
<Suspense fallback={<SkeletonTableFindings />}>
<SSRDataTable searchParams={searchParams} />
</Suspense>
</>
);
}
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 (
<DataTableFindings
columns={ColumnsFindings}
data={[
{
id: "12345",
attributes: {
CheckTitle:
"Ensure users of groups with AdministratorAccess policy have MFA tokens enabled",
severity: "critical",
status: "fail",
region: "us-west-2",
service: "cloudformation",
account: "dev (106908755756)",
},
card: {
resourceId:
"StackSet-AWSControlTowerBP-BASELINE-CLOUDWATCH-57c2a54c-9a36-4af9-a910-d1adb424c62a",
resourceLink:
"https://app.prowler.pro/app/findings?date=2024-08-05&search=StackSet-AWSControlTowerBP-BASELINE-CLOUDWATCH-57c2a54c-9a36-4af9-a910-d1adb424c62a",
resourceARN:
"arn:aws:cloudformation:eu-west-1:714274078102:stack/StackSet-AWSControlTowerBP-BASELINE-CLOUDWATCH-57c2a54c-9a36-4af9-a910-d1adb424c62a/9656eda0-909c-11ec-8fb2-06f4f86422d5",
checkId: "cloudformation_stack_outputs_find_secrets",
checkLink:
"https://app.prowler.pro/app/findings?date=2024-08-05&search=cloudformation_stack_outputs_find_secrets",
type: ["Not applicable"],
scanTime: "2024-07-17T09:55:14.191475Z",
findingId: "ba123291-03a5-49a1-b962-6fdb1d2b9c9b",
findingLink:
"https://app.prowler.pro/app/findings?date=2024-08-05&search=ba123291-03a5-49a1-b962-6fdb1d2b9c9b",
details:
"Potential secret found in Stack StackSet-AWSControlTowerBP-BASELINE-CLOUDWATCH-57c2a54c-9a36-4af9-a910-d1adb424c62a Outputs.",
riskLink:
"https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html",
riskDetails:
"Secrets hardcoded into CloudFormation outputs can be used by malware and bad actors to gain lateral access to other services.",
recommendationLink:
"https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-secret-generatesecretstring.html",
recommendationDetails:
"Implement automated detective control to scan accounts for passwords and secrets. Use secrets manager service to store and retrieve passwords and secrets.",
referenceInformation: "CLI",
referenceLink:
"https://docs.prowler.com/checks/aws/secrets-policies/bc_aws_secrets_2/#cli-command",
},
},
{
id: "67891",
attributes: {
CheckTitle: "Find secrets in CloudFormation outputs",
severity: "low",
status: "success",
region: "us-east-1",
service: "cloudformation",
account: "stg (987654321987)",
},
card: {
resourceId: "<root_account>",
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",
},
},
]}
<DataTable
columns={ColumnFindings}
data={expandedResponse?.data || []}
metadata={findingsData?.meta}
customFilters={filterFindings}
/>
);
};
+19
View File
@@ -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
];
-93
View File
@@ -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<FindingsCardProps> = ({ 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 (
<>
<div
className={clsx(
"max-h-[calc(100vh-146px)] overflow-y-auto rounded-md border bg-background transition-all duration-300",
{
"ml-2 w-1/3 translate-x-0 p-3 opacity-100": selectedRow,
"w-0 translate-x-full opacity-0": !selectedRow,
},
)}
>
<p className="mb-3 font-bold">{CheckTitle}</p>
<Divider />
<FindingsCardContent
title="Resource ID:"
url={resourceLink}
description={resourceId}
/>
<FindingsCardContent title="Resource ARN:" description={resourceARN} />
<FindingsCardContent
title="Check ID:"
url={checkLink}
description={checkId}
/>
<FindingsCardType type={type} />
<FindingsCardScan title="Scan Time:" dateTime={scanTime} />
<FindingsCardContent
title="Prowler Finding ID:"
url={findingLink}
description={findingId}
/>
<FindingsCardDetail title="Details:" description={details} />
<FindingsCardDetail
title="Risk:"
url={riskLink}
description={riskDetails}
type="risk"
/>
<FindingsCardDetail
title="Recommendation:"
url={recommendationLink}
description={recommendationDetails}
type="recommendation"
/>
<FindingsCardDetail
title="Reference Information:"
url={referenceLink}
description={referenceInformation}
type="reference"
/>
</div>
</>
);
};
@@ -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<FindingsCardContentProps> = ({
title,
url,
description,
}) => {
return (
<>
<p className="mt-3 text-sm font-bold">{title}</p>
{url ? (
<Link className="text-sm" href={url}>
{description}
</Link>
) : (
<p className="text-sm">{description}</p>
)}
</>
);
};
@@ -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<FindingsCardDetailProps> = ({
title,
url,
description,
type = "default",
}) => {
return (
<>
{description && (
<div
className={`mt-3 rounded-md border-2 text-sm ${getDetailColorClass(type)} p-2`}
>
<p className="mb-2 flex items-center justify-between font-bold">
<span>{title}</span>
{url && (
<Button
href={url}
as={Link}
color="primary"
variant="flat"
isExternal
size="sm"
>
View Source
</Button>
)}
</p>
<p>{description}</p>
</div>
)}
</>
);
};
-25
View File
@@ -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<FindingsCardScanProps> = ({
title,
dateTime = "",
}) => {
const date = dateTime && parseISO(dateTime);
const formattedDate = date && format(date, "MMM dd, yyyy");
const formattedTime = date && format(date, "p 'UTC'");
return (
<>
<p className="mt-3 text-sm font-bold">{title}</p>
<p className="text-sm">
{formattedDate} at {formattedTime}
</p>
</>
);
};
-25
View File
@@ -1,25 +0,0 @@
import React from "react";
interface FindingsCardTypeProps {
type: string[];
}
export const FindingsCardType: React.FC<FindingsCardTypeProps> = ({
type = [],
}) => {
const typeContent = () => {
if (type.length > 0) {
return type.join(", ");
}
return type[0];
};
return (
<>
<p className="mt-3 text-sm font-bold">
{type.length > 1 ? "Types:" : "Type:"}
</p>
<p className="text-sm">{typeContent()}</p>
</>
);
};
-8
View File
@@ -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";
@@ -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<FindingProps>[] = [
{
accessorKey: "checkTitle",
header: "Check",
cell: ({ row }) => {
const { CheckTitle } = getFindingsAttributes(row);
return <p className="text-sm">{CheckTitle}</p>;
},
},
{
accessorKey: "severity",
header: "Severity",
cell: ({ row }) => {
const { severity } = getFindingsAttributes(row);
return <SeverityBadge severity={severity} />;
},
},
{
accessorKey: "region",
header: "Region",
cell: ({ row }) => {
const { region } = getFindingsAttributes(row);
return <p className="text-nowrap text-sm">{region}</p>;
},
},
{
accessorKey: "service",
header: "Service",
cell: ({ row }) => {
const { service } = getFindingsAttributes(row);
return <p className="text-sm">{service}</p>;
},
},
{
accessorKey: "account",
header: "Account",
cell: ({ row }) => {
const { account } = getFindingsAttributes(row);
return <p className="text-sm">{account}</p>;
},
},
{
accessorKey: "actions",
header: () => (
<div className="relative flex items-center justify-end gap-2">
<Dropdown className="border-1 border-default-200 bg-background">
<DropdownTrigger>
<Button isIconOnly radius="full" size="sm" variant="light">
<VerticalDotsIcon className="text-default-400" />
</Button>
</DropdownTrigger>
<DropdownMenu>
<DropdownItem>Mute Findings for Selected Filters</DropdownItem>
<DropdownItem>Configure Muted Findings</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
),
},
];
@@ -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<TData extends FindingProps, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}
export function DataTableFindings<TData extends FindingProps, TValue>({
columns,
data,
}: DataTableFindingsProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
const selectedRow = table.getSelectedRowModel().rows[0]?.original;
const onClick = (row: Row<TData>) => {
table.resetRowSelection(true);
row.toggleSelected();
return;
};
return (
<>
<div className="flex w-full flex-row">
<div
className={clsx("transition-all duration-300", {
"w-2/3": selectedRow,
"w-full": !selectedRow,
})}
>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
onClick={() => onClick(row)}
className={"hover:cursor-pointer"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="solid"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="solid"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
<FindingsCard selectedRow={selectedRow} />
</div>
</>
);
}
@@ -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<FindingProps>[] = [
{
accessorKey: "check",
header: "Check",
cell: ({ row }) => {
const { checktitle } = getFindingsMetadata(row);
return <p className="max-w-96 truncate text-medium">{checktitle}</p>;
},
},
{
accessorKey: "scanName",
header: "Scan Name",
cell: ({ row }) => {
const name = getScanData(row, "name");
return (
<p className="max-w-96 truncate text-medium">
{typeof name === "string" || typeof name === "number"
? name
: "Invalid data"}
</p>
);
},
},
{
accessorKey: "severity",
header: "Severity",
cell: ({ row }) => {
const {
attributes: { severity },
} = getFindingsData(row);
return <SeverityBadge severity={severity} />;
},
},
{
accessorKey: "status",
header: "Scan Status",
cell: ({ row }) => {
const {
attributes: { status },
} = getFindingsData(row);
const mappedStatus = statusMap[status];
return <StatusBadge status={mappedStatus} />;
},
},
{
accessorKey: "region",
header: "Region",
cell: ({ row }) => {
const region = getResourceData(row, "region");
return (
<>
<div>{typeof region === "string" ? region : "Invalid region"}</div>
</>
);
},
},
{
accessorKey: "service",
header: "Service",
cell: ({ row }) => {
const { servicename } = getFindingsMetadata(row);
return <p className="max-w-96 truncate text-medium">{servicename}</p>;
},
},
{
accessorKey: "account",
header: "Account",
cell: ({ row }) => {
const account = getProviderData(row, "uid");
return (
<>
<div>{typeof account === "string" ? account : "Invalid account"}</div>
</>
);
},
},
{
id: "actions",
cell: ({ row }) => {
return <DataTableRowActions row={row} />;
},
},
];
@@ -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<FindingProps> {
row: Row<FindingProps>;
}
const iconClasses =
"text-2xl text-default-500 pointer-events-none flex-shrink-0";
export function DataTableRowActions<FindingProps>({
row,
}: DataTableRowActionsProps<FindingProps>) {
// 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 (
<>
{/* <CustomAlertModal
isOpen={isEditOpen}
onOpenChange={setIsEditOpen}
title="Edit Provider"
description={"Edit the provider details"}
>
<EditForm
providerId={providerId}
providerAlias={providerAlias}
setIsOpen={setIsEditOpen}
/>
</CustomAlertModal>
<CustomAlertModal
isOpen={isDeleteOpen}
onOpenChange={setIsDeleteOpen}
title="Are you absolutely sure?"
description="This action cannot be undone. This will permanently delete your provider account and remove your data from the server."
>
<DeleteForm providerId={providerId} setIsOpen={setIsDeleteOpen} />
</CustomAlertModal> */}
<div className="relative flex items-center justify-end gap-2">
<Dropdown className="shadow-xl" placement="bottom">
<DropdownTrigger>
<Button isIconOnly radius="full" size="sm" variant="light">
<VerticalDotsIcon className="text-default-400" />
</Button>
</DropdownTrigger>
<DropdownMenu
closeOnSelect
aria-label="Actions"
color="default"
variant="flat"
>
<DropdownSection title="Actions">
<DropdownItem
key="jira"
description="Allows you to send the finding to Jira"
textValue="Send to Jira"
startContent={<EditDocumentBulkIcon className={iconClasses} />}
// onClick={() => setIsEditOpen(true)}
>
Send to Jira
</DropdownItem>
<DropdownItem
key="slack"
description="Allows you to send the finding to Slack"
textValue="Send to Slack"
startContent={<EditDocumentBulkIcon className={iconClasses} />}
// onClick={() => setIsEditOpen(true)}
>
Send to Slack
</DropdownItem>
</DropdownSection>
</DropdownMenu>
</Dropdown>
</div>
</>
);
}
+3
View File
@@ -0,0 +1,3 @@
export * from "./column-findings";
export * from "./data-table-row-actions";
export * from "./skeleton-table-findings";
+1 -1
View File
@@ -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: <AlertIcon size={14} className="mr-1" />,
+1 -1
View File
@@ -1,7 +1,7 @@
import { Chip } from "@nextui-org/react";
import React from "react";
type Status =
export type Status =
| "available"
| "scheduled"
| "executing"
+12
View File
@@ -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);
+1 -1
View File
@@ -1,2 +1,2 @@
export * from "./custom";
export * from "./helper";
export * from "./utils";
+136 -23
View File
@@ -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<string, string>;
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;
};
}