diff --git a/app/(prowler)/page.tsx b/app/(prowler)/page.tsx
index 7cf4e2273f..2c2c286166 100644
--- a/app/(prowler)/page.tsx
+++ b/app/(prowler)/page.tsx
@@ -2,6 +2,7 @@ import { Spacer } from "@nextui-org/react";
import { StatusChart } from "@/components/charts";
import { FilterControls } from "@/components/filters";
+import { AttackSurface } from "@/components/overview";
import { Header } from "@/components/ui";
import { CustomBox } from "@/components/ui/custom";
@@ -29,7 +30,7 @@ export default function Home() {
preTitle={"Attack Surface"}
className="col-span-12 sm:col-span-12 xl:col-span-4 3xl:col-span-5"
>
-
hi hi
+
>
diff --git a/components/charts/StatusChart.tsx b/components/charts/StatusChart.tsx
index ec0253b529..316820aa3d 100644
--- a/components/charts/StatusChart.tsx
+++ b/components/charts/StatusChart.tsx
@@ -39,22 +39,10 @@ const chartConfig = {
number: {
label: "Findings",
},
- chrome: {
- label: "Chrome",
- color: "hsl(var(--chart-1))",
- },
success: {
label: "Success",
color: "hsl(var(--chart-success))",
},
- firefox: {
- label: "Firefox",
- color: "hsl(var(--chart-3))",
- },
- edge: {
- label: "Edge",
- color: "hsl(var(--chart-4))",
- },
fail: {
label: "Fail",
color: "hsl(var(--chart-fail))",
diff --git a/components/overview/AttackSurface.tsx b/components/overview/AttackSurface.tsx
new file mode 100644
index 0000000000..61bb2fdfd2
--- /dev/null
+++ b/components/overview/AttackSurface.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+
+import ActionCard from "../ui/action-card/ActionCard";
+
+const cardData = [
+ {
+ findings: 3,
+ icon: "solar:danger-triangle-bold",
+ title: "Internet Exposed Resources",
+ },
+ {
+ findings: 15,
+ icon: "solar:danger-triangle-bold",
+ title: "Exposed Secrets",
+ },
+ {
+ findings: 0,
+ icon: "heroicons:shield-check-solid",
+ title: "IAM Policies Leading to Privilege Escalation",
+ },
+ {
+ findings: 0,
+ icon: "heroicons:shield-check-solid",
+ title: "EC2 with Metadata Service V1 (IMDSv1)",
+ },
+];
+
+export const AttackSurface = () => {
+ return (
+
+ {cardData.map((card, index) => (
+
0 ? "fail" : "success"}
+ icon={card.findings > 0 ? "solar:danger-triangle-bold" : card.icon}
+ title={card.title}
+ description={
+ card.findings > 0 ? "Review Required" : "No Issues Found"
+ }
+ />
+ ))}
+
+ );
+};
diff --git a/components/overview/index.ts b/components/overview/index.ts
new file mode 100644
index 0000000000..452749b068
--- /dev/null
+++ b/components/overview/index.ts
@@ -0,0 +1 @@
+export * from "./AttackSurface";
diff --git a/components/ui/action-card/ActionCard.tsx b/components/ui/action-card/ActionCard.tsx
new file mode 100644
index 0000000000..e4c0234ffd
--- /dev/null
+++ b/components/ui/action-card/ActionCard.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import { Icon } from "@iconify/react";
+import type { CardProps } from "@nextui-org/react";
+import { Card, CardBody } from "@nextui-org/react";
+import React from "react";
+
+import { cn } from "@/lib";
+
+export type ActionCardProps = CardProps & {
+ icon: string;
+ title: string;
+ color?: "success" | "secondary" | "warning" | "fail";
+ description: string;
+};
+
+const ActionCard = React.forwardRef(
+ ({ color, title, icon, description, children, className, ...props }, ref) => {
+ const colors = React.useMemo(() => {
+ switch (color) {
+ case "success":
+ return {
+ card: "border-system-success-medium",
+ iconWrapper: "bg-system-success-lighter border-system-success",
+ icon: "text-system-success",
+ };
+ case "secondary":
+ return {
+ card: "border-secondary-100",
+ iconWrapper: "bg-secondary-50 border-secondary-100",
+ icon: "text-secondary",
+ };
+ case "warning":
+ return {
+ card: "border-warning-500",
+ iconWrapper: "bg-warning-50 border-warning-100",
+ icon: "text-warning-600",
+ };
+ case "fail":
+ return {
+ card: "border-danger-300",
+ iconWrapper: "bg-danger-50 border-danger-100",
+ icon: "text-danger",
+ };
+
+ default:
+ return {
+ card: "border-default-200",
+ iconWrapper: "bg-default-50 border-default-100",
+ icon: "text-default-500",
+ };
+ }
+ }, [color]);
+
+ return (
+
+
+
+
+
+
+
{title}
+
+ {description || children}
+
+
+
+
+ );
+ },
+);
+
+ActionCard.displayName = "ActionCard";
+
+export default ActionCard;
diff --git a/components/ui/index.ts b/components/ui/index.ts
index 86e8f0143a..f026779b96 100644
--- a/components/ui/index.ts
+++ b/components/ui/index.ts
@@ -1,3 +1,4 @@
+export * from "./action-card/ActionCard";
export * from "./alert/Alert";
export * from "./chart/Chart";
export * from "./dialog/Dialog";
diff --git a/lib/utils.ts b/lib/utils.ts
index 84a5fe24e3..c236eefe6a 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -1,12 +1,27 @@
import { type ClassValue, clsx } from "clsx";
-import { twMerge } from "tailwind-merge";
+import { extendTailwindMerge } from "tailwind-merge";
+const COMMON_UNITS = ["small", "medium", "large"];
import { MetaDataProps } from "@/types";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
-
+const twMerge = extendTailwindMerge({
+ extend: {
+ theme: {
+ opacity: ["disabled"],
+ spacing: ["divider"],
+ borderWidth: COMMON_UNITS,
+ borderRadius: COMMON_UNITS,
+ },
+ classGroups: {
+ shadow: [{ shadow: COMMON_UNITS }],
+ "font-size": [{ text: ["tiny", ...COMMON_UNITS] }],
+ "bg-image": ["bg-stripe-gradient"],
+ },
+ },
+});
export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value));
export const convertFileToUrl = (file: File) => URL.createObjectURL(file);
diff --git a/tailwind.config.js b/tailwind.config.js
index 333f9f5d0f..7bf6fecf2c 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -18,6 +18,34 @@ module.exports = {
},
},
extend: {
+ colors: {
+ system: {
+ success: {
+ DEFAULT: "#09BF3D",
+ medium: "#3CEC6D",
+ light: "#B5FDC8",
+ lighter: "#D9FFE3",
+ },
+ error: {
+ DEFAULT: "#E11D48",
+ medium: "#FB718F",
+ light: "#FECDD8",
+ lighter: "#FFE4EA",
+ },
+ info: {
+ DEFAULT: "#7C3AED",
+ medium: "#B48BFA",
+ light: "#E5D6FE",
+ lighter: "#F1E9FE",
+ },
+ warning: {
+ DEFAULT: "#FBBF24",
+ medium: "#FDDD8A",
+ light: "#feefc7",
+ lighter: "#FFF9EB",
+ },
+ },
+ },
fontFamily: {
sans: ["var(--font-sans)"],
mono: ["var(--font-geist-mono)"],
@@ -31,6 +59,27 @@ module.exports = {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
+ advance: { from: { width: 0 }, to: { width: "100%" } },
+ "fade-in": { from: { opacity: 0 }, to: { opacity: 1 } },
+ "fade-out": { from: { opacity: 1 }, to: { opacity: 0 } },
+ "slide-in": {
+ from: { transform: "translateX(100%)" },
+ to: { transform: "translateX(0)" },
+ },
+ "slide-out": {
+ from: { transform: "translateX(0)" },
+ to: { transform: "translateX(100%)" },
+ },
+ woosh: {
+ "0, 10%": { left: 0, right: "100%" },
+ "40%, 60%": { left: 0, right: 0 },
+ "90%, 100%": { left: "100%", right: 0 },
+ },
+ lineAnim: {
+ "0%": { left: "-40%" },
+ "50%": { left: "20%", width: "80%" },
+ "100%": { left: "100%", width: "100%" },
+ },
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",