diff --git a/ui/.dockerignore b/ui/.dockerignore
new file mode 100644
index 0000000000..df076390d2
--- /dev/null
+++ b/ui/.dockerignore
@@ -0,0 +1,15 @@
+# Include any files or directories that you don't want to be copied to your
+# container here (e.g., local build artifacts, temporary files, etc.).
+#
+# For more help, visit the .dockerignore file reference guide at
+# https://docs.docker.com/go/build-context-dockerignore/
+
+Dockerfile
+.dockerignore
+node_modules
+npm-debug.log
+README.md
+.next
+!.next/static
+!.next/standalone
+.git
diff --git a/ui/.env.template b/ui/.env.template
new file mode 100644
index 0000000000..ba212ad68a
--- /dev/null
+++ b/ui/.env.template
@@ -0,0 +1,6 @@
+SITE_URL=http://localhost:3000
+API_BASE_URL=http://localhost:8080/api/v1
+AUTH_TRUST_HOST=true
+
+# openssl rand -base64 32
+AUTH_SECRET=your-secret-key
diff --git a/ui/.eslintignore b/ui/.eslintignore
new file mode 100644
index 0000000000..af6ab76f80
--- /dev/null
+++ b/ui/.eslintignore
@@ -0,0 +1,20 @@
+.now/*
+*.css
+.changeset
+dist
+esm/*
+public/*
+tests/*
+scripts/*
+*.config.js
+.DS_Store
+node_modules
+coverage
+.next
+build
+!.commitlintrc.cjs
+!.lintstagedrc.cjs
+!jest.config.js
+!plopfile.js
+!react-shim.js
+!tsup.config.ts
\ No newline at end of file
diff --git a/ui/.eslintrc.cjs b/ui/.eslintrc.cjs
new file mode 100644
index 0000000000..6f1426da12
--- /dev/null
+++ b/ui/.eslintrc.cjs
@@ -0,0 +1,44 @@
+module.exports = {
+ env: {
+ node: true,
+ es2021: true,
+ },
+ parser: "@typescript-eslint/parser",
+ plugins: ["prettier", "@typescript-eslint", "simple-import-sort", "jsx-a11y"],
+ extends: [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:security/recommended-legacy",
+ "plugin:jsx-a11y/recommended",
+ "eslint-config-prettier",
+ "prettier",
+ ],
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ rules: {
+ "no-console": 1,
+ eqeqeq: 2,
+ quotes: ["error", "double", "avoid-escape"],
+ "@typescript-eslint/no-explicit-any": "off",
+ "security/detect-object-injection": "off",
+ "prettier/prettier": [
+ "error",
+ {
+ endOfLine: "auto",
+ tabWidth: 2,
+ useTabs: false,
+ },
+ ],
+ "eol-last": ["error", "always"],
+ "simple-import-sort/imports": "error",
+ "simple-import-sort/exports": "error",
+ "jsx-a11y/anchor-is-valid": "error",
+ "jsx-a11y/alt-text": "error",
+ "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
+ },
+};
diff --git a/ui/.github/CODEOWNERS b/ui/.github/CODEOWNERS
new file mode 100644
index 0000000000..2ad99bd49a
--- /dev/null
+++ b/ui/.github/CODEOWNERS
@@ -0,0 +1,5 @@
+* @prowler-cloud/ui
+
+# To protect a repository fully against unauthorized changes, you also need to define an owner for the CODEOWNERS file itself.
+# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-and-branch-protection
+/.github/ @prowler-cloud/ui
diff --git a/ui/.github/pull_request_template.md b/ui/.github/pull_request_template.md
new file mode 100644
index 0000000000..4bf3030402
--- /dev/null
+++ b/ui/.github/pull_request_template.md
@@ -0,0 +1,7 @@
+### Description
+
+What was done in this PR
+
+### How to Review
+
+The reviewer should verify all these steps:
diff --git a/ui/.github/workflows/checks.yml b/ui/.github/workflows/checks.yml
new file mode 100644
index 0000000000..e58e18c793
--- /dev/null
+++ b/ui/.github/workflows/checks.yml
@@ -0,0 +1,29 @@
+name: CI - Code Quality and Health Check
+
+on:
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ test-and-coverage:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ node-version: [20.x]
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+ with:
+ persist-credentials: false
+ - name: Setup Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: Install dependencies
+ run: npm install
+ - name: Run Healthcheck
+ run: npm run healthcheck
+ - name: Build the application
+ run: npm run build
diff --git a/ui/.gitignore b/ui/.gitignore
new file mode 100644
index 0000000000..45c1abce86
--- /dev/null
+++ b/ui/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+.env
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/ui/.husky/pre-commit b/ui/.husky/pre-commit
new file mode 100644
index 0000000000..129b71298f
--- /dev/null
+++ b/ui/.husky/pre-commit
@@ -0,0 +1 @@
+npm run healthcheck
diff --git a/ui/.npmrc b/ui/.npmrc
new file mode 100644
index 0000000000..cafe685a11
--- /dev/null
+++ b/ui/.npmrc
@@ -0,0 +1 @@
+package-lock=true
diff --git a/ui/.nvmrc b/ui/.nvmrc
new file mode 100644
index 0000000000..b009dfb9d9
--- /dev/null
+++ b/ui/.nvmrc
@@ -0,0 +1 @@
+lts/*
diff --git a/ui/.prettierignore b/ui/.prettierignore
new file mode 100644
index 0000000000..40b878db5b
--- /dev/null
+++ b/ui/.prettierignore
@@ -0,0 +1 @@
+node_modules/
\ No newline at end of file
diff --git a/ui/.prettierrc.json b/ui/.prettierrc.json
new file mode 100644
index 0000000000..0cbbafa6cc
--- /dev/null
+++ b/ui/.prettierrc.json
@@ -0,0 +1,10 @@
+{
+ "bracketSpacing": true,
+ "singleQuote": false,
+ "trailingComma": "all",
+ "tabWidth": 2,
+ "useTabs": false,
+ "semi": true,
+ "printWidth": 80,
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
diff --git a/ui/.vscode/settings.json b/ui/.vscode/settings.json
new file mode 100644
index 0000000000..104c678edb
--- /dev/null
+++ b/ui/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "prettier.printWidth": 80,
+ "prettier.tabWidth": 2,
+ "prettier.useTabs": false,
+ "prettier.singleQuote": false,
+ "prettier.trailingComma": "all",
+ "prettier.semi": true,
+ "prettier.bracketSpacing": true
+}
diff --git a/ui/Dockerfile b/ui/Dockerfile
new file mode 100644
index 0000000000..a8c698c014
--- /dev/null
+++ b/ui/Dockerfile
@@ -0,0 +1,70 @@
+FROM node:20-alpine AS base
+
+LABEL maintainer="https://github.com/prowler-cloud"
+# Install dependencies only when needed
+FROM base AS deps
+# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
+#hadolint ignore=DL3018
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+
+# Install dependencies based on the preferred package manager
+COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
+RUN \
+ if [ -f package-lock.json ]; then npm install; \
+ else echo "Lockfile not found." && exit 1; \
+ fi
+
+
+# Rebuild the source code only when needed
+FROM base AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+
+# Next.js collects completely anonymous telemetry data about general usage.
+# Learn more here: https://nextjs.org/telemetry
+# Uncomment the following line in case you want to disable telemetry during the build.
+ENV NEXT_TELEMETRY_DISABLED=1
+
+RUN \
+ if [ -f package-lock.json ]; then npm run build; \
+ else echo "Lockfile not found." && exit 1; \
+ fi
+
+# Development stage
+FROM base AS dev
+WORKDIR /app
+
+# Set up environment for development
+ENV NODE_ENV=development
+ENV NEXT_TELEMETRY_DISABLED=1
+COPY --from=builder /app /app
+
+# Run development server with hot-reloading
+CMD ["npm", "run", "dev"]
+
+# Production stage
+FROM base AS prod
+WORKDIR /app
+
+# Set up environment for production
+ENV NODE_ENV=production
+ENV NEXT_TELEMETRY_DISABLED=1
+
+RUN addgroup --system --gid 1001 nodejs &&\
+adduser --system --uid 1001 nextjs
+
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+COPY --from=builder --chown=nextjs:nodejs /app/public ./public
+
+USER nextjs
+
+EXPOSE 3000
+
+ENV PORT=3000
+ENV HOSTNAME="0.0.0.0"
+# server.js is created by next build from the standalone output
+# https://nextjs.org/docs/pages/api-reference/next-config-js/output
+CMD ["node", "server.js"]
diff --git a/ui/README.md b/ui/README.md
new file mode 100644
index 0000000000..3e334368ff
--- /dev/null
+++ b/ui/README.md
@@ -0,0 +1,114 @@
+# Description
+
+This repository hosts the UI component for Prowler, providing a user-friendly web interface to interact seamlessly with Prowler's features.
+
+
+## ๐ Production deployment
+### Docker deployment
+#### Clone the repository
+```console
+# HTTPS
+git clone https://github.com/prowler-cloud/ui.git
+
+# SSH
+git clone git@github.com:prowler-cloud/ui.git
+
+```
+#### Build the Docker image
+```bash
+docker build -t prowler-cloud/ui . --target prod
+```
+#### Run the Docker container
+```bash
+docker run -p 3000:3000 prowler-cloud/ui
+```
+
+### Local deployment
+#### Clone the repository
+
+```console
+# HTTPS
+git clone https://github.com/prowler-cloud/ui.git
+
+# SSH
+git clone git@github.com:prowler-cloud/ui.git
+
+```
+
+#### Build the project
+
+```bash
+npm run build
+```
+
+#### Run the production server
+
+```bash
+npm start
+```
+
+## ๐งช Development deployment
+### Docker deployment
+#### Clone the repository
+```console
+# HTTPS
+git clone https://github.com/prowler-cloud/ui.git
+
+# SSH
+git clone git@github.com:prowler-cloud/ui.git
+
+```
+#### Build the Docker image
+```bash
+docker build -t prowler-cloud/ui . --target dev
+```
+#### Run the Docker container
+```bash
+docker run -p 3000:3000 prowler-cloud/ui
+```
+
+### Local deployment
+#### Clone the repository
+
+```console
+# HTTPS
+git clone https://github.com/prowler-cloud/ui.git
+
+# SSH
+git clone git@github.com:prowler-cloud/ui.git
+
+```
+
+#### Install dependencies
+
+You can use one of them `npm`, `yarn`, `pnpm`, `bun`, Example using `npm`:
+
+```bash
+npm install
+```
+
+#### Run the development server
+
+```bash
+npm run dev
+```
+
+## Setup pnpm (optional)
+
+If you are using `pnpm`, you need to add the following code to your `.npmrc` file:
+
+```bash
+public-hoist-pattern[]=*@nextui-org/*
+```
+
+After modifying the `.npmrc` file, you need to run `pnpm install` again to ensure that the dependencies are installed correctly.
+
+## Technologies Used
+
+- [Next.js 14](https://nextjs.org/docs/getting-started)
+- [NextUI v2](https://nextui.org/)
+- [Tailwind CSS](https://tailwindcss.com/)
+- [Tailwind Variants](https://tailwind-variants.org)
+- [TypeScript](https://www.typescriptlang.org/)
+- [Framer Motion](https://www.framer.com/motion/)
+- [next-themes](https://github.com/pacocoursey/next-themes)
diff --git a/ui/actions/auth/auth.ts b/ui/actions/auth/auth.ts
new file mode 100644
index 0000000000..d5ec199b06
--- /dev/null
+++ b/ui/actions/auth/auth.ts
@@ -0,0 +1,169 @@
+"use server";
+
+import { AuthError } from "next-auth";
+import { z } from "zod";
+
+import { signIn, signOut } from "@/auth.config";
+import { authFormSchema } from "@/types";
+
+const formSchemaSignIn = authFormSchema("sign-in");
+const formSchemaSignUp = authFormSchema("sign-up");
+
+const defaultValues: z.infer = {
+ email: "",
+ password: "",
+};
+
+export async function authenticate(
+ prevState: unknown,
+ formData: z.infer,
+) {
+ try {
+ await signIn("credentials", {
+ ...formData,
+ redirect: false,
+ });
+ return {
+ message: "Success",
+ };
+ } catch (error) {
+ if (error instanceof AuthError) {
+ switch (error.type) {
+ case "CredentialsSignin":
+ return {
+ message: "Credentials error",
+ errors: {
+ ...defaultValues,
+ credentials: "Incorrect email or password",
+ },
+ };
+ default:
+ return {
+ message: "Unknown error",
+ errors: {
+ ...defaultValues,
+ unknown: "Unknown error",
+ },
+ };
+ }
+ }
+ }
+}
+
+export const createNewUser = async (
+ formData: z.infer,
+) => {
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/users`);
+
+ if (formData.invitationToken) {
+ url.searchParams.append("invitation_token", formData.invitationToken);
+ }
+
+ const bodyData = {
+ data: {
+ type: "users",
+ attributes: {
+ name: formData.name,
+ email: formData.email,
+ password: formData.password,
+ ...(formData.company && { company_name: formData.company }),
+ },
+ },
+ };
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ },
+ body: JSON.stringify(bodyData),
+ });
+
+ const parsedResponse = await response.json();
+ if (!response.ok) {
+ return parsedResponse;
+ }
+
+ return parsedResponse;
+ } catch (error) {
+ return { errors: [{ detail: "Network error or server is unreachable" }] };
+ }
+};
+
+export const getToken = async (formData: z.infer) => {
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/tokens`);
+
+ const bodyData = {
+ data: {
+ type: "tokens",
+ attributes: {
+ email: formData.email,
+ password: formData.password,
+ },
+ },
+ };
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ },
+ body: JSON.stringify(bodyData),
+ });
+
+ if (!response.ok) return null;
+
+ const parsedResponse = await response.json();
+
+ const accessToken = parsedResponse.data.attributes.access;
+ const refreshToken = parsedResponse.data.attributes.refresh;
+ return {
+ accessToken,
+ refreshToken,
+ };
+ } catch (error) {
+ throw new Error("Error in trying to get token");
+ }
+};
+
+export const getUserByMe = async (accessToken: string) => {
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/users/me`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "GET",
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ if (!response.ok) throw new Error("Error in trying to get user by me");
+
+ const parsedResponse = await response.json();
+
+ const name = parsedResponse.data.attributes.name;
+ const email = parsedResponse.data.attributes.email;
+ const company = parsedResponse.data.attributes.company_name;
+ const dateJoined = parsedResponse.data.attributes.date_joined;
+ return {
+ name,
+ email,
+ company,
+ dateJoined,
+ };
+ } catch (error) {
+ throw new Error("Error in trying to get user by me");
+ }
+};
+
+export async function logOut() {
+ await signOut();
+}
diff --git a/ui/actions/auth/index.ts b/ui/actions/auth/index.ts
new file mode 100644
index 0000000000..97ccf76494
--- /dev/null
+++ b/ui/actions/auth/index.ts
@@ -0,0 +1 @@
+export * from "./auth";
diff --git a/ui/actions/compliances/compliances.ts b/ui/actions/compliances/compliances.ts
new file mode 100644
index 0000000000..e9c8814c31
--- /dev/null
+++ b/ui/actions/compliances/compliances.ts
@@ -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;
+ }
+};
diff --git a/ui/actions/compliances/index.ts b/ui/actions/compliances/index.ts
new file mode 100644
index 0000000000..ba9df7e860
--- /dev/null
+++ b/ui/actions/compliances/index.ts
@@ -0,0 +1 @@
+export * from "./compliances";
diff --git a/ui/actions/findings/findings.ts b/ui/actions/findings/findings.ts
new file mode 100644
index 0000000000..9fc89b0907
--- /dev/null
+++ b/ui/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/ui/actions/findings/index.ts b/ui/actions/findings/index.ts
new file mode 100644
index 0000000000..eb3a674c67
--- /dev/null
+++ b/ui/actions/findings/index.ts
@@ -0,0 +1 @@
+export * from "./findings";
diff --git a/ui/actions/invitations/index.ts b/ui/actions/invitations/index.ts
new file mode 100644
index 0000000000..addd041884
--- /dev/null
+++ b/ui/actions/invitations/index.ts
@@ -0,0 +1 @@
+export * from "./invitation";
diff --git a/ui/actions/invitations/invitation.ts b/ui/actions/invitations/invitation.ts
new file mode 100644
index 0000000000..56aefc74e1
--- /dev/null
+++ b/ui/actions/invitations/invitation.ts
@@ -0,0 +1,174 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { redirect } from "next/navigation";
+
+import { auth } from "@/auth.config";
+import { getErrorMessage, parseStringify, wait } from "@/lib";
+
+export const getInvitations = async ({
+ page = 1,
+ query = "",
+ sort = "",
+ filters = {},
+}) => {
+ const session = await auth();
+
+ if (isNaN(Number(page)) || page < 1) redirect("/invitations");
+
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/tenants/invitations`);
+
+ 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 invitations = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await invitations.json();
+ const parsedData = parseStringify(data);
+ revalidatePath("/invitations");
+ return parsedData;
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error("Error fetching invitations:", error);
+ return undefined;
+ }
+};
+
+export const sendInvite = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+
+ const email = formData.get("email");
+ const url = new URL(`${keyServer}/tenants/invitations`);
+
+ const body = JSON.stringify({
+ data: {
+ type: "invitations",
+ attributes: {
+ email,
+ },
+ relationships: {},
+ },
+ });
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ body,
+ });
+ const data = await response.json();
+
+ return parseStringify(data);
+ } catch (error) {
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const updateInvite = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+
+ const invitationId = formData.get("invitationId");
+ const invitationEmail = formData.get("invitationEmail");
+ const expiresAt = formData.get("expires_at");
+
+ const url = new URL(`${keyServer}/tenants/invitations/${invitationId}`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ body: JSON.stringify({
+ data: {
+ type: "invitations",
+ id: invitationId,
+ attributes: {
+ email: invitationEmail,
+ ...(expiresAt && { expires_at: expiresAt }),
+ },
+ },
+ }),
+ });
+ const data = await response.json();
+ revalidatePath("/invitations");
+ return parseStringify(data);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error("Error updating invitation:", error);
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const getInvitationInfoById = async (invitationId: string) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/tenants/invitations/${invitationId}`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "GET",
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+
+ const data = await response.json();
+ return parseStringify(data);
+ } catch (error) {
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const revokeInvite = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+
+ const invitationId = formData.get("invitationId");
+ const url = new URL(`${keyServer}/tenants/invitations/${invitationId}`);
+ try {
+ const response = await fetch(url.toString(), {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await response.json();
+ await wait(1000);
+ revalidatePath("/invitations");
+ return parseStringify(data);
+ } catch (error) {
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
diff --git a/ui/actions/overview/overview.ts b/ui/actions/overview/overview.ts
new file mode 100644
index 0000000000..8d83ab7088
--- /dev/null
+++ b/ui/actions/overview/overview.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 getProvidersOverview = async ({
+ page = 1,
+ query = "",
+ sort = "",
+ filters = {},
+}) => {
+ const session = await auth();
+
+ if (isNaN(Number(page)) || page < 1) redirect("/providers-overview");
+
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/overviews/providers`);
+
+ 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 response = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+
+ const data = await response.json();
+ const parsedData = parseStringify(data);
+ revalidatePath("/providers-overview");
+ return parsedData;
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error("Error fetching providers overview:", error);
+ return undefined;
+ }
+};
diff --git a/ui/actions/providers/index.ts b/ui/actions/providers/index.ts
new file mode 100644
index 0000000000..5532383f5f
--- /dev/null
+++ b/ui/actions/providers/index.ts
@@ -0,0 +1 @@
+export * from "./providers";
diff --git a/ui/actions/providers/providers.ts b/ui/actions/providers/providers.ts
new file mode 100644
index 0000000000..fc5cd97bb7
--- /dev/null
+++ b/ui/actions/providers/providers.ts
@@ -0,0 +1,322 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { redirect } from "next/navigation";
+
+import { auth } from "@/auth.config";
+import { getErrorMessage, parseStringify, wait } from "@/lib";
+
+export const getProviders = async ({
+ page = 1,
+ query = "",
+ sort = "",
+ filters = {},
+}) => {
+ const session = await auth();
+
+ if (isNaN(Number(page)) || page < 1) redirect("/providers");
+
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/providers`);
+
+ 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 providers = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await providers.json();
+ const parsedData = parseStringify(data);
+ revalidatePath("/providers");
+ return parsedData;
+ } catch (error) {
+ console.error("Error fetching providers:", error);
+ return undefined;
+ }
+};
+
+export const getProvider = async (formData: FormData) => {
+ const session = await auth();
+ const providerId = formData.get("id");
+
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/providers/${providerId}`);
+
+ try {
+ const providers = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await providers.json();
+ const parsedData = parseStringify(data);
+ return parsedData;
+ } catch (error) {
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const updateProvider = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+
+ const providerId = formData.get("providerId");
+ const providerAlias = formData.get("alias");
+
+ const url = new URL(`${keyServer}/providers/${providerId}`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ body: JSON.stringify({
+ data: {
+ type: "providers",
+ id: providerId,
+ attributes: {
+ alias: providerAlias,
+ },
+ },
+ }),
+ });
+ const data = await response.json();
+ revalidatePath("/providers");
+ return parseStringify(data);
+ } catch (error) {
+ console.error(error);
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const addProvider = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+
+ const providerType = formData.get("providerType");
+ const providerUid = formData.get("providerUid");
+ const providerAlias = formData.get("providerAlias");
+
+ const url = new URL(`${keyServer}/providers`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ body: JSON.stringify({
+ data: {
+ type: "providers",
+ attributes: {
+ provider: providerType,
+ uid: providerUid,
+ alias: providerAlias,
+ },
+ },
+ }),
+ });
+ const data = await response.json();
+ revalidatePath("/providers");
+ return parseStringify(data);
+ } catch (error) {
+ console.error(error);
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const addCredentialsProvider = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/providers/secrets`);
+
+ const secretName = formData.get("secretName");
+ const providerId = formData.get("providerId");
+ const providerType = formData.get("providerType");
+
+ const isRole = formData.get("role_arn") !== null;
+
+ let secret = {};
+ let secretType = "static"; // Default to static credentials
+
+ if (providerType === "aws") {
+ if (isRole) {
+ // Role-based configuration for AWS
+ secretType = "role";
+ secret = {
+ role_arn: formData.get("role_arn"),
+ aws_access_key_id: formData.get("aws_access_key_id") || undefined,
+ aws_secret_access_key:
+ formData.get("aws_secret_access_key") || undefined,
+ aws_session_token: formData.get("aws_session_token") || undefined,
+ session_duration:
+ parseInt(formData.get("session_duration") as string, 10) || 3600,
+ external_id: formData.get("external_id") || undefined,
+ role_session_name: formData.get("role_session_name") || undefined,
+ };
+ } else {
+ // Static credentials configuration for AWS
+ secret = {
+ aws_access_key_id: formData.get("aws_access_key_id"),
+ aws_secret_access_key: formData.get("aws_secret_access_key"),
+ aws_session_token: formData.get("aws_session_token") || undefined,
+ };
+ }
+ } else if (providerType === "azure") {
+ // Static credentials configuration for Azure
+ secret = {
+ client_id: formData.get("client_id"),
+ client_secret: formData.get("client_secret"),
+ tenant_id: formData.get("tenant_id"),
+ };
+ } else if (providerType === "gcp") {
+ // Static credentials configuration for GCP
+ secret = {
+ client_id: formData.get("client_id"),
+ client_secret: formData.get("client_secret"),
+ refresh_token: formData.get("refresh_token"),
+ };
+ } else if (providerType === "kubernetes") {
+ // Static credentials configuration for Kubernetes
+ secret = {
+ kubeconfig_content: formData.get("kubeconfig_content"),
+ };
+ }
+
+ const bodyData = {
+ data: {
+ type: "provider-secrets",
+ attributes: {
+ secret_type: secretType,
+ secret,
+ name: secretName,
+ },
+ relationships: {
+ provider: {
+ data: {
+ id: providerId,
+ type: "providers",
+ },
+ },
+ },
+ },
+ };
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ body: JSON.stringify(bodyData),
+ });
+ const data = await response.json();
+ revalidatePath("/providers");
+ return parseStringify(data);
+ } catch (error) {
+ console.error(error);
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const checkConnectionProvider = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+
+ const providerId = formData.get("providerId");
+
+ const url = new URL(`${keyServer}/providers/${providerId}/connection`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await response.json();
+ await wait(1000);
+ revalidatePath("/providers");
+ return parseStringify(data);
+ } catch (error) {
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const deleteCredentials = async (secretId: string) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/providers/secrets/${secretId}`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await response.json();
+ revalidatePath("/providers");
+ return parseStringify(data);
+ } catch (error) {
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const deleteProvider = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+
+ const providerId = formData.get("id");
+ const url = new URL(`${keyServer}/providers/${providerId}`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await response.json();
+ await wait(1000);
+ revalidatePath("/providers");
+ return parseStringify(data);
+ } catch (error) {
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
diff --git a/ui/actions/scans/index.ts b/ui/actions/scans/index.ts
new file mode 100644
index 0000000000..7e4b1aceeb
--- /dev/null
+++ b/ui/actions/scans/index.ts
@@ -0,0 +1 @@
+export * from "./scans";
diff --git a/ui/actions/scans/scans.ts b/ui/actions/scans/scans.ts
new file mode 100644
index 0000000000..367446daaa
--- /dev/null
+++ b/ui/actions/scans/scans.ts
@@ -0,0 +1,156 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { redirect } from "next/navigation";
+
+import { auth } from "@/auth.config";
+import { getErrorMessage, parseStringify } from "@/lib";
+
+export const getScans = async ({
+ page = 1,
+ query = "",
+ sort = "",
+ filters = {},
+}) => {
+ const session = await auth();
+
+ if (isNaN(Number(page)) || page < 1) redirect("/scans");
+
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/scans`);
+
+ 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 scans = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await scans.json();
+ const parsedData = parseStringify(data);
+ revalidatePath("/scans");
+ return parsedData;
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error("Error fetching scans:", error);
+ return undefined;
+ }
+};
+
+export const getScan = async (scanId: string) => {
+ const session = await auth();
+
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/scans/${scanId}`);
+
+ try {
+ const scan = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await scan.json();
+ const parsedData = parseStringify(data);
+
+ return parsedData;
+ } catch (error) {
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const scanOnDemand = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+
+ const providerId = formData.get("providerId");
+ const scanName = formData.get("scanName");
+
+ const url = new URL(`${keyServer}/scans`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ body: JSON.stringify({
+ data: {
+ type: "scans",
+ attributes: {
+ name: scanName,
+ },
+ relationships: {
+ provider: {
+ data: {
+ type: "providers",
+ id: providerId,
+ },
+ },
+ },
+ },
+ }),
+ });
+ const data = await response.json();
+ revalidatePath("/scans");
+ return parseStringify(data);
+ } catch (error) {
+ console.error(error);
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const updateScan = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+
+ const scanId = formData.get("scanId");
+ const scanName = formData.get("scanName");
+
+ const url = new URL(`${keyServer}/scans/${scanId}`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ body: JSON.stringify({
+ data: {
+ type: "scans",
+ id: scanId,
+ attributes: {
+ name: scanName,
+ },
+ },
+ }),
+ });
+ const data = await response.json();
+ revalidatePath("/scans");
+ return parseStringify(data);
+ } catch (error) {
+ console.error(error);
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
diff --git a/ui/actions/services/index.ts b/ui/actions/services/index.ts
new file mode 100644
index 0000000000..b2221a94a8
--- /dev/null
+++ b/ui/actions/services/index.ts
@@ -0,0 +1 @@
+export * from "./services";
diff --git a/ui/actions/services/services.ts b/ui/actions/services/services.ts
new file mode 100644
index 0000000000..c4ab342f01
--- /dev/null
+++ b/ui/actions/services/services.ts
@@ -0,0 +1,99 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { redirect } from "next/navigation";
+import { parse } from "path";
+
+import { auth } from "@/auth.config";
+import { getErrorMessage, parseStringify } from "@/lib";
+
+export const getServices = async ({}) => {
+ const session = await auth();
+
+ const keyServer = process.env.API_BASE_URL;
+ const servicesToFetch = [
+ { id: "accessanalyzer", alias: "IAM Access Analyzer" },
+ { id: "account", alias: "AWS Account" },
+ { id: "acm", alias: "AWS Certificate Manager" },
+ { id: "apigateway", alias: "Amazon API Gateway" },
+ { id: "apigatewayv2", alias: "Amazon API Gateway V2" },
+ { id: "athena", alias: "Amazon Athena" },
+ { id: "autoscaling", alias: "Amazon EC2 Auto Scaling" },
+ { id: "awslambda", alias: "AWS Lambda" },
+ { id: "backup", alias: "AWS Backup" },
+ { id: "cloudformation", alias: "AWS CloudFormation" },
+ { id: "cloudfront", alias: "Amazon CloudFront" },
+ { id: "cloudtrail", alias: "AWS CloudTrail" },
+ { id: "cloudwatch", alias: "Amazon CloudWatch" },
+ { id: "codeartifact", alias: "AWS CodeArtifact" },
+ { id: "codebuild", alias: "AWS CodeBuild" },
+ { id: "config", alias: "AWS Config" },
+ { id: "dlm", alias: "Amazon Data Lifecycle Manager" },
+ { id: "drs", alias: "AWS Data Replication Service" },
+ { id: "dynamodb", alias: "Amazon DynamoDB" },
+ { id: "ec2", alias: "Amazon EC2" },
+ { id: "ecr", alias: "Amazon ECR" },
+ { id: "ecs", alias: "Amazon ECS" },
+ { id: "efs", alias: "Amazon EFS" },
+ { id: "eks", alias: "Amazon EKS" },
+ { id: "elasticache", alias: "Amazon ElastiCache" },
+ { id: "elb", alias: "Elastic Load Balancing" },
+ { id: "elbv2", alias: "Elastic Load Balancing v2" },
+ { id: "emr", alias: "Amazon EMR" },
+ { id: "fms", alias: "AWS Firewall Manager" },
+ { id: "glacier", alias: "Amazon Glacier" },
+ { id: "glue", alias: "AWS Glue" },
+ { id: "guardduty", alias: "Amazon GuardDuty" },
+ { id: "iam", alias: "AWS IAM" },
+ { id: "inspector2", alias: "Amazon Inspector" },
+ { id: "kms", alias: "AWS KMS" },
+ { id: "macie", alias: "Amazon Macie" },
+ { id: "networkfirewall", alias: "AWS Network Firewall" },
+ { id: "organizations", alias: "AWS Organizations" },
+ { id: "rds", alias: "Amazon RDS" },
+ { id: "resourceexplorer2", alias: "AWS Resource Groups" },
+ { id: "route53", alias: "Amazon Route 53" },
+ { id: "s3", alias: "Amazon S3" },
+ { id: "secretsmanager", alias: "AWS Secrets Manager" },
+ { id: "securityhub", alias: "AWS Security Hub" },
+ { id: "sns", alias: "Amazon SNS" },
+ { id: "sqs", alias: "Amazon SQS" },
+ { id: "ssm", alias: "AWS Systems Manager" },
+ { id: "ssmincidents", alias: "AWS Systems Manager Incident Manager" },
+ { id: "trustedadvisor", alias: "AWS Trusted Advisor" },
+ { id: "vpc", alias: "Amazon VPC" },
+ { id: "wafv2", alias: "AWS WAF" },
+ { id: "wellarchitected", alias: "AWS Well-Architected Tool" },
+ ];
+
+ const parsedData = [];
+
+ for (const service of servicesToFetch) {
+ const url = new URL(`${keyServer}/findings`);
+ url.searchParams.append("filter[service]", service.id);
+ url.searchParams.append("filter[status]", "FAIL");
+
+ try {
+ const response = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+
+ const data = await response.json();
+ const failFindings = data.meta.pagination.count;
+
+ parsedData.push({
+ service_id: service.id,
+ service_alias: service.alias,
+ fail_findings: failFindings,
+ });
+ } catch (error) {
+ console.error(`Error fetching data for service ${service.id}:`, error);
+ }
+ }
+
+ revalidatePath("/services");
+ return parsedData;
+};
diff --git a/ui/actions/task/index.ts b/ui/actions/task/index.ts
new file mode 100644
index 0000000000..078ba8fc62
--- /dev/null
+++ b/ui/actions/task/index.ts
@@ -0,0 +1 @@
+export * from "./tasks";
diff --git a/ui/actions/task/tasks.ts b/ui/actions/task/tasks.ts
new file mode 100644
index 0000000000..fdf75c7cea
--- /dev/null
+++ b/ui/actions/task/tasks.ts
@@ -0,0 +1,24 @@
+"use server";
+
+import { auth } from "@/auth.config";
+import { getErrorMessage, parseStringify } from "@/lib";
+
+export const getTask = async (taskId: string) => {
+ const session = await auth();
+
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/tasks/${taskId}`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await response.json();
+ return parseStringify(data);
+ } catch (error) {
+ return { error: getErrorMessage(error) };
+ }
+};
diff --git a/ui/actions/users/users.ts b/ui/actions/users/users.ts
new file mode 100644
index 0000000000..30a5b26d1f
--- /dev/null
+++ b/ui/actions/users/users.ts
@@ -0,0 +1,116 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { redirect } from "next/navigation";
+
+import { auth } from "@/auth.config";
+import { getErrorMessage, parseStringify, wait } from "@/lib";
+
+export const getUsers = async ({
+ page = 1,
+ query = "",
+ sort = "",
+ filters = {},
+}) => {
+ const session = await auth();
+
+ if (isNaN(Number(page)) || page < 1) redirect("/users");
+
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/users`);
+
+ 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 users = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await users.json();
+ const parsedData = parseStringify(data);
+ revalidatePath("/users");
+ return parsedData;
+ } catch (error) {
+ console.error("Error fetching users:", error);
+ return undefined;
+ }
+};
+
+export const updateUser = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+
+ const userId = formData.get("userId");
+ const userName = formData.get("name");
+ const userPassword = formData.get("password");
+ const userEmail = formData.get("email");
+ const userCompanyName = formData.get("company_name");
+
+ const url = new URL(`${keyServer}/users/${userId}`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ body: JSON.stringify({
+ data: {
+ type: "users",
+ id: userId,
+ attributes: {
+ name: userName,
+ password: userPassword,
+ email: userEmail,
+ company_name: userCompanyName,
+ },
+ },
+ }),
+ });
+ const data = await response.json();
+ revalidatePath("/users");
+ return parseStringify(data);
+ } catch (error) {
+ console.error(error);
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const deleteUser = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+
+ const userId = formData.get("userId");
+ const url = new URL(`${keyServer}/users/${userId}`);
+ try {
+ const response = await fetch(url.toString(), {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await response.json();
+ await wait(1000);
+ revalidatePath("/users");
+ return parseStringify(data);
+ } catch (error) {
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
diff --git a/ui/app/(auth)/layout.tsx b/ui/app/(auth)/layout.tsx
new file mode 100644
index 0000000000..a9fdc8c394
--- /dev/null
+++ b/ui/app/(auth)/layout.tsx
@@ -0,0 +1,60 @@
+import "@/styles/globals.css";
+
+import { Metadata, Viewport } from "next";
+import { redirect } from "next/navigation";
+
+import { auth } from "@/auth.config";
+import { Toaster } from "@/components/ui";
+import { fontSans } from "@/config/fonts";
+import { siteConfig } from "@/config/site";
+import { cn } from "@/lib";
+
+import { Providers } from "../providers";
+
+export const metadata: Metadata = {
+ title: {
+ default: siteConfig.name,
+ template: `%s - ${siteConfig.name}`,
+ },
+ description: siteConfig.description,
+ icons: {
+ icon: "/favicon.ico",
+ },
+};
+
+export const viewport: Viewport = {
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "white" },
+ { media: "(prefers-color-scheme: dark)", color: "black" },
+ ],
+};
+
+export default async function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const session = await auth();
+
+ if (session?.user) {
+ redirect("/");
+ }
+
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/ui/app/(auth)/sign-in/page.tsx b/ui/app/(auth)/sign-in/page.tsx
new file mode 100644
index 0000000000..b5bd978029
--- /dev/null
+++ b/ui/app/(auth)/sign-in/page.tsx
@@ -0,0 +1,7 @@
+import { AuthForm } from "@/components/auth/oss";
+
+const SignIn = () => {
+ return ;
+};
+
+export default SignIn;
diff --git a/ui/app/(auth)/sign-up/page.tsx b/ui/app/(auth)/sign-up/page.tsx
new file mode 100644
index 0000000000..28149bb200
--- /dev/null
+++ b/ui/app/(auth)/sign-up/page.tsx
@@ -0,0 +1,13 @@
+import { AuthForm } from "@/components/auth/oss";
+import { SearchParamsProps } from "@/types";
+
+const SignUp = ({ searchParams }: { searchParams: SearchParamsProps }) => {
+ const invitationToken =
+ typeof searchParams?.invitation_token === "string"
+ ? searchParams.invitation_token
+ : null;
+
+ return ;
+};
+
+export default SignUp;
diff --git a/ui/app/(prowler)/categories/page.tsx b/ui/app/(prowler)/categories/page.tsx
new file mode 100644
index 0000000000..1a5650d8af
--- /dev/null
+++ b/ui/app/(prowler)/categories/page.tsx
@@ -0,0 +1,12 @@
+import { Spacer } from "@nextui-org/react";
+
+import { Header } from "@/components/ui";
+
+export default async function Categories() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/ui/app/(prowler)/compliance/page.tsx b/ui/app/(prowler)/compliance/page.tsx
new file mode 100644
index 0000000000..38c9800467
--- /dev/null
+++ b/ui/app/(prowler)/compliance/page.tsx
@@ -0,0 +1,122 @@
+import { Spacer } from "@nextui-org/react";
+import { Suspense } from "react";
+
+import { getCompliancesOverview } from "@/actions/compliances";
+import { getScans } from "@/actions/scans";
+import {
+ ComplianceCard,
+ ComplianceSkeletonGrid,
+} from "@/components/compliance";
+import { DataCompliance } from "@/components/compliance/data-compliance";
+import { Header } from "@/components/ui";
+import { ComplianceOverviewData, SearchParamsProps } from "@/types";
+
+export default async function Compliance({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) {
+ const scansData = await getScans({});
+ const scanList = scansData?.data
+ .filter(
+ (scan: any) =>
+ scan.attributes.state === "completed" &&
+ scan.attributes.progress === 100,
+ )
+ .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 (
+ <>
+
+
+
+
+ }>
+
+
+ >
+ );
+}
+
+const SSRComplianceGrid = async ({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) => {
+ const scanId = searchParams.scanId?.toString() || "";
+
+ 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 (
+
+
+ No compliance data available for the selected scan.
+
+
+ );
+ }
+
+ // Handle errors returned by the API
+ if (compliancesData?.errors?.length > 0) {
+ return (
+
+
Provide a valid scan ID.
+
+ );
+ }
+
+ return (
+
+ {compliancesData.data.map((compliance: ComplianceOverviewData) => {
+ const { attributes } = compliance;
+ const {
+ framework,
+ requirements_status: { passed, total },
+ } = attributes;
+
+ return (
+
+ );
+ })}
+
+ );
+};
diff --git a/ui/app/(prowler)/error.tsx b/ui/app/(prowler)/error.tsx
new file mode 100644
index 0000000000..980e681220
--- /dev/null
+++ b/ui/app/(prowler)/error.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import Link from "next/link";
+import { useEffect } from "react";
+
+import { RocketIcon } from "@/components/icons";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui";
+
+export default function Error({
+ error,
+ // reset,
+}: {
+ error: Error;
+ reset: () => void;
+}) {
+ useEffect(() => {
+ // Log the error to an error reporting service
+ /* eslint-disable no-console */
+ console.error(error);
+ }, [error]);
+
+ return (
+
+
+ An unexpected error occurred
+
+ We're sorry for the inconvenience. Please try again or contact support
+ if the problem persists.
+
+
+ {" "}
+ Go to the homepage
+
+
+ );
+}
diff --git a/ui/app/(prowler)/findings/page.tsx b/ui/app/(prowler)/findings/page.tsx
new file mode 100644
index 0000000000..53c0795f23
--- /dev/null
+++ b/ui/app/(prowler)/findings/page.tsx
@@ -0,0 +1,174 @@
+import { Spacer } from "@nextui-org/react";
+import React, { Suspense } from "react";
+
+import { getFindings } from "@/actions/findings";
+import { getProviders } from "@/actions/providers";
+import { getScans } from "@/actions/scans";
+import { filterFindings } from "@/components/filters/data-filters";
+import { FilterControls } from "@/components/filters/filter-controls";
+import {
+ ColumnFindings,
+ SkeletonTableFindings,
+} from "@/components/findings/table";
+import { Header } from "@/components/ui";
+import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
+import { createDict } from "@/lib";
+import { FindingProps, SearchParamsProps } from "@/types/components";
+
+export default async function Findings({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) {
+ const searchParamsKey = JSON.stringify(searchParams || {});
+
+ // Get findings data
+ const findingsData = await getFindings({});
+ const providersData = await getProviders({});
+ const scansData = await getScans({});
+
+ // Extract provider UIDs
+ const providerUIDs = providersData?.data
+ ?.map((provider: any) => provider.attributes.uid)
+ .filter(Boolean);
+
+ // Extract scan UUIDs with "completed" state and more than one resource
+ const completedScans = scansData?.data
+ ?.filter(
+ (scan: any) =>
+ scan.attributes.state === "completed" &&
+ scan.attributes.unique_resource_count > 1 &&
+ scan.attributes.name, // Ensure it has a name
+ )
+ .map((scan: any) => ({
+ id: scan.id,
+ name: scan.attributes.name,
+ }));
+
+ const completedScanIds = completedScans?.map((scan: any) => scan.id) || [];
+
+ // Create resource dictionary
+ const resourceDict = createDict("resources", findingsData);
+
+ // Get unique regions and services
+ const allRegionsAndServices =
+ findingsData?.data
+ ?.flatMap((finding: FindingProps) => {
+ const resource =
+ resourceDict[finding.relationships?.resources?.data?.[0]?.id];
+ return {
+ region: resource?.attributes?.region,
+ service: resource?.attributes?.service,
+ };
+ })
+ .filter(Boolean) || [];
+
+ const uniqueRegions = Array.from(
+ new Set(
+ allRegionsAndServices
+ .map((item: { region: string }) => item.region)
+ .filter(Boolean) || [],
+ ),
+ );
+ const uniqueServices = Array.from(
+ new Set(
+ allRegionsAndServices
+ .map((item: { service: string }) => item.service)
+ .filter(Boolean) || [],
+ ),
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+
+ }>
+
+
+ >
+ );
+}
+
+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("resources", findingsData);
+ const scanDict = createDict("scans", findingsData);
+ const providerDict = createDict("providers", findingsData);
+
+ // Expand each finding with its corresponding resource, scan, and provider
+ const expandedFindings = findingsData?.data
+ ? 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 (
+
+ );
+};
diff --git a/ui/app/(prowler)/integrations/page.tsx b/ui/app/(prowler)/integrations/page.tsx
new file mode 100644
index 0000000000..a930f1ed36
--- /dev/null
+++ b/ui/app/(prowler)/integrations/page.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+
+import { Header } from "@/components/ui";
+
+export default function Integrations() {
+ return (
+ <>
+
+
+ Hi hi from Integration page
+ >
+ );
+}
diff --git a/ui/app/(prowler)/invitations/(send-invite)/check-details/page.tsx b/ui/app/(prowler)/invitations/(send-invite)/check-details/page.tsx
new file mode 100644
index 0000000000..939667cdc1
--- /dev/null
+++ b/ui/app/(prowler)/invitations/(send-invite)/check-details/page.tsx
@@ -0,0 +1,43 @@
+import { Suspense } from "react";
+
+import { getInvitationInfoById } from "@/actions/invitations/invitation";
+import { InvitationDetails } from "@/components/invitations";
+import { SkeletonInvitationInfo } from "@/components/invitations/workflow";
+import { SearchParamsProps } from "@/types";
+
+export default async function CheckDetailsPage({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) {
+ const searchParamsKey = JSON.stringify(searchParams || {});
+
+ return (
+ }>
+
+
+ );
+}
+
+const SSRDataInvitation = async ({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) => {
+ const invitationId = searchParams.id;
+
+ if (!invitationId) {
+ return Invalid invitation ID
;
+ }
+
+ const invitationData = (await getInvitationInfoById(invitationId as string))
+ .data;
+
+ if (!invitationData) {
+ return Invitation not found
;
+ }
+
+ const { attributes, links } = invitationData;
+
+ return ;
+};
diff --git a/ui/app/(prowler)/invitations/(send-invite)/layout.tsx b/ui/app/(prowler)/invitations/(send-invite)/layout.tsx
new file mode 100644
index 0000000000..2af2be2af2
--- /dev/null
+++ b/ui/app/(prowler)/invitations/(send-invite)/layout.tsx
@@ -0,0 +1,32 @@
+import "@/styles/globals.css";
+
+import { Spacer } from "@nextui-org/react";
+import React from "react";
+
+import { WorkflowSendInvite } from "@/components/invitations/workflow";
+import { NavigationHeader } from "@/components/ui";
+
+interface InvitationLayoutProps {
+ children: React.ReactNode;
+}
+
+export default function InvitationLayout({ children }: InvitationLayoutProps) {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/ui/app/(prowler)/invitations/(send-invite)/new/page.tsx b/ui/app/(prowler)/invitations/(send-invite)/new/page.tsx
new file mode 100644
index 0000000000..87a2e33acd
--- /dev/null
+++ b/ui/app/(prowler)/invitations/(send-invite)/new/page.tsx
@@ -0,0 +1,7 @@
+import React from "react";
+
+import { SendInvitationForm } from "@/components/invitations/workflow/forms/send-invitation-form";
+
+export default function SendInvitationPage() {
+ return ;
+}
diff --git a/ui/app/(prowler)/invitations/page.tsx b/ui/app/(prowler)/invitations/page.tsx
new file mode 100644
index 0000000000..b18cbbcba3
--- /dev/null
+++ b/ui/app/(prowler)/invitations/page.tsx
@@ -0,0 +1,66 @@
+import { Spacer } from "@nextui-org/react";
+import { Suspense } from "react";
+
+import { getInvitations } from "@/actions/invitations/invitation";
+import { FilterControls } from "@/components/filters";
+import { filterInvitations } from "@/components/filters/data-filters";
+import { SendInvitationButton } from "@/components/invitations";
+import {
+ ColumnsInvitation,
+ SkeletonTableInvitation,
+} from "@/components/invitations/table";
+import { Header } from "@/components/ui";
+import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
+import { SearchParamsProps } from "@/types";
+
+export default async function Invitations({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) {
+ const searchParamsKey = JSON.stringify(searchParams || {});
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ }>
+
+
+ >
+ );
+}
+
+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 invitationsData = await getInvitations({ query, page, sort, filters });
+
+ return (
+
+ );
+};
diff --git a/ui/app/(prowler)/layout.tsx b/ui/app/(prowler)/layout.tsx
new file mode 100644
index 0000000000..40a2fd9bf9
--- /dev/null
+++ b/ui/app/(prowler)/layout.tsx
@@ -0,0 +1,58 @@
+import "@/styles/globals.css";
+
+import { Metadata, Viewport } from "next";
+import React from "react";
+
+import { SidebarWrap, Toaster } from "@/components/ui";
+import { fontSans } from "@/config/fonts";
+import { siteConfig } from "@/config/site";
+import { cn } from "@/lib/utils";
+
+import { Providers } from "../providers";
+
+export const metadata: Metadata = {
+ title: {
+ default: siteConfig.name,
+ template: `%s - ${siteConfig.name}`,
+ },
+ description: siteConfig.description,
+ icons: {
+ icon: "/favicon.ico",
+ },
+};
+
+export const viewport: Viewport = {
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "white" },
+ { media: "(prefers-color-scheme: dark)", color: "black" },
+ ],
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+ );
+}
diff --git a/ui/app/(prowler)/page.tsx b/ui/app/(prowler)/page.tsx
new file mode 100644
index 0000000000..bae0d835c4
--- /dev/null
+++ b/ui/app/(prowler)/page.tsx
@@ -0,0 +1,42 @@
+import { Spacer } from "@nextui-org/react";
+import { Suspense } from "react";
+
+import { getProvidersOverview } from "@/actions/overview/overview";
+import {
+ ProvidersOverview,
+ SkeletonProvidersOverview,
+} from "@/components/overview";
+import { Header } from "@/components/ui";
+
+export default function Home() {
+ return (
+ <>
+
+
+
+
+ {/* Providers Overview */}
+
+ }>
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+const SSRProvidersOverview = async () => {
+ const providersOverview = await getProvidersOverview({});
+
+ if (!providersOverview) {
+ return There is no providers overview info available
;
+ }
+
+ return ;
+};
diff --git a/ui/app/(prowler)/profile/page.tsx b/ui/app/(prowler)/profile/page.tsx
new file mode 100644
index 0000000000..0cfb4f8ab2
--- /dev/null
+++ b/ui/app/(prowler)/profile/page.tsx
@@ -0,0 +1,30 @@
+import { Spacer } from "@nextui-org/react";
+import { redirect } from "next/navigation";
+import React from "react";
+
+// import { getUserByMe } from "@/actions/auth/auth";
+import { auth } from "@/auth.config";
+import { Header } from "@/components/ui";
+
+export default async function Profile() {
+ const session = await auth();
+
+ if (!session?.user) {
+ // redirect("/sign-in?returnTo=/profile");
+ redirect("/sign-in");
+ }
+
+ // const user = await getUserByMe();
+
+ return (
+ <>
+
+
+
+ {JSON.stringify(session.user, null, 2)}
+ {JSON.stringify(session.userId, null, 2)}
+ {JSON.stringify(session.tenantId, null, 2)}
+ {JSON.stringify(session, null, 2)}
+ >
+ );
+}
diff --git a/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx
new file mode 100644
index 0000000000..6a525990fa
--- /dev/null
+++ b/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx
@@ -0,0 +1,35 @@
+import { redirect } from "next/navigation";
+import React from "react";
+
+import {
+ ViaCredentialsForm,
+ ViaRoleForm,
+} from "@/components/providers/workflow/forms";
+
+interface Props {
+ searchParams: { type: string; id: string; via?: string };
+}
+
+export default function AddCredentialsPage({ searchParams }: Props) {
+ if (
+ !searchParams.type ||
+ !searchParams.id ||
+ (searchParams.type === "aws" && !searchParams.via)
+ ) {
+ redirect("/providers/connect-account");
+ }
+
+ const useCredentialsForm =
+ (searchParams.type === "aws" && searchParams.via === "credentials") ||
+ (searchParams.type !== "aws" && !searchParams.via);
+
+ const useRoleForm =
+ searchParams.type === "aws" && searchParams.via === "role";
+
+ return (
+ <>
+ {useCredentialsForm && }
+ {useRoleForm && }
+ >
+ );
+}
diff --git a/ui/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx
new file mode 100644
index 0000000000..eead6c2561
--- /dev/null
+++ b/ui/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx
@@ -0,0 +1,7 @@
+import React from "react";
+
+import { ConnectAccountForm } from "@/components/providers/workflow/forms";
+
+export default function ConnectAccountPage() {
+ return ;
+}
diff --git a/ui/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx
new file mode 100644
index 0000000000..3e88a5ea46
--- /dev/null
+++ b/ui/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx
@@ -0,0 +1,32 @@
+import { redirect } from "next/navigation";
+import React from "react";
+
+import { getProvider } from "@/actions/providers";
+import { LaunchScanForm } from "@/components/providers/workflow/forms";
+
+interface Props {
+ searchParams: { type: string; id: string };
+}
+
+export default async function LaunchScanPage({ searchParams }: Props) {
+ const providerId = searchParams.id;
+
+ if (!providerId) {
+ redirect("/providers/connect-account");
+ }
+
+ const formData = new FormData();
+ formData.append("id", providerId);
+
+ const providerData = await getProvider(formData);
+
+ const isConnected = providerData?.data?.attributes?.connection?.connected;
+
+ if (!isConnected) {
+ redirect("/providers/connect-account");
+ }
+
+ return (
+
+ );
+}
diff --git a/ui/app/(prowler)/providers/(set-up-provider)/layout.tsx b/ui/app/(prowler)/providers/(set-up-provider)/layout.tsx
new file mode 100644
index 0000000000..85b78ee687
--- /dev/null
+++ b/ui/app/(prowler)/providers/(set-up-provider)/layout.tsx
@@ -0,0 +1,32 @@
+import "@/styles/globals.css";
+
+import { Spacer } from "@nextui-org/react";
+import React from "react";
+
+import { WorkflowAddProvider } from "@/components/providers/workflow";
+import { NavigationHeader } from "@/components/ui";
+
+interface ProviderLayoutProps {
+ children: React.ReactNode;
+}
+
+export default function ProviderLayout({ children }: ProviderLayoutProps) {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/ui/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx
new file mode 100644
index 0000000000..13cac50f6d
--- /dev/null
+++ b/ui/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx
@@ -0,0 +1,44 @@
+import { redirect } from "next/navigation";
+import React, { Suspense } from "react";
+
+import { getProvider } from "@/actions/providers";
+import { TestConnectionForm } from "@/components/providers/workflow/forms";
+
+interface Props {
+ searchParams: { type: string; id: string };
+}
+
+export default async function TestConnectionPage({ searchParams }: Props) {
+ const providerId = searchParams.id;
+
+ if (!providerId) {
+ redirect("/providers/connect-account");
+ }
+
+ return (
+ Loading...
}>
+
+
+ );
+}
+
+async function SSRTestConnection({
+ searchParams,
+}: {
+ searchParams: { type: string; id: string };
+}) {
+ const formData = new FormData();
+ formData.append("id", searchParams.id);
+
+ const providerData = await getProvider(formData);
+ if (providerData.errors) {
+ redirect("/providers/connect-account");
+ }
+
+ return (
+
+ );
+}
diff --git a/ui/app/(prowler)/providers/page.tsx b/ui/app/(prowler)/providers/page.tsx
new file mode 100644
index 0000000000..7aae9df08d
--- /dev/null
+++ b/ui/app/(prowler)/providers/page.tsx
@@ -0,0 +1,65 @@
+import { Spacer } from "@nextui-org/react";
+import { Suspense } from "react";
+
+import { getProviders } from "@/actions/providers";
+import { FilterControls, filterProviders } from "@/components/filters";
+import { AddProvider } from "@/components/providers";
+import {
+ ColumnProviders,
+ SkeletonTableProviders,
+} from "@/components/providers/table";
+import { Header } from "@/components/ui";
+import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
+import { SearchParamsProps } from "@/types";
+
+export default async function Providers({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) {
+ const searchParamsKey = JSON.stringify(searchParams || {});
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ }>
+
+
+ >
+ );
+}
+
+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 providersData = await getProviders({ query, page, sort, filters });
+ return (
+
+ );
+};
diff --git a/ui/app/(prowler)/scans/page.tsx b/ui/app/(prowler)/scans/page.tsx
new file mode 100644
index 0000000000..5b92328cb8
--- /dev/null
+++ b/ui/app/(prowler)/scans/page.tsx
@@ -0,0 +1,104 @@
+import { Spacer } from "@nextui-org/react";
+import { Suspense } from "react";
+
+import { getProviders } from "@/actions/providers";
+import { getScans } from "@/actions/scans";
+import { filterScans } from "@/components/filters";
+import { ButtonRefreshData } from "@/components/scans";
+import { LaunchScanWorkflow } from "@/components/scans/launch-workflow";
+import { SkeletonTableScans } from "@/components/scans/table";
+import { ColumnGetScans } from "@/components/scans/table/scans";
+import { Header } from "@/components/ui";
+import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
+import { ProviderProps, SearchParamsProps } from "@/types";
+
+export default async function Scans({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) {
+ const filteredParams = { ...searchParams };
+ delete filteredParams.scanId;
+ const searchParamsKey = JSON.stringify(filteredParams);
+
+ const providersData = await getProviders({});
+
+ const providerInfo = providersData?.data?.length
+ ? providersData.data.map((provider: ProviderProps) => ({
+ providerId: provider.id,
+ alias: provider.attributes.alias,
+ providerType: provider.attributes.provider,
+ uid: provider.attributes.uid,
+ connected: provider.attributes.connection.connected,
+ }))
+ : [];
+
+ // const executingScans = await getExecutingScans();
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {
+ "use server";
+ await getScans({});
+ }}
+ />
+
+
+
+
+
+ >
+ );
+}
+
+const SSRDataTableScans = async ({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) => {
+ const page = parseInt(searchParams.page?.toString() || "1", 10);
+ const sort = searchParams.sort?.toString();
+
+ // Extract all filter parameters, excluding scanId
+ const filters = Object.fromEntries(
+ Object.entries(searchParams).filter(
+ ([key]) => key.startsWith("filter[") && key !== "scanId",
+ ),
+ );
+
+ // Extract query from filters
+ const query = (filters["filter[search]"] as string) || "";
+
+ const scansData = await getScans({ query, page, sort, filters });
+
+ return (
+
+ );
+};
+
+// const getExecutingScans = async () => {
+// const scansData = await getScans({});
+
+// return scansData?.data?.some(
+// (scan: ScanProps) =>
+// scan.attributes.state === "executing" && scan.attributes.progress < 100,
+// );
+// };
diff --git a/ui/app/(prowler)/services/page.tsx b/ui/app/(prowler)/services/page.tsx
new file mode 100644
index 0000000000..1772165913
--- /dev/null
+++ b/ui/app/(prowler)/services/page.tsx
@@ -0,0 +1,51 @@
+import { Spacer } from "@nextui-org/react";
+import { Suspense } from "react";
+
+import { getServices } from "@/actions/services";
+import { FilterControls } from "@/components/filters";
+import { ServiceCard, ServiceSkeletonGrid } from "@/components/services";
+import { Header } from "@/components/ui";
+import { SearchParamsProps } from "@/types";
+
+export default async function Services({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) {
+ const searchParamsKey = JSON.stringify(searchParams || {});
+ return (
+ <>
+
+
+
+
+ }>
+
+
+ >
+ );
+}
+
+const SSRServiceGrid = async ({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) => {
+ const servicesData = await getServices(searchParams);
+ const [services] = await Promise.all([servicesData]);
+
+ return (
+
+ {services?.map((service: any) => (
+
+ ))}
+
+ );
+};
diff --git a/ui/app/(prowler)/settings/page.tsx b/ui/app/(prowler)/settings/page.tsx
new file mode 100644
index 0000000000..6653c38b1c
--- /dev/null
+++ b/ui/app/(prowler)/settings/page.tsx
@@ -0,0 +1,12 @@
+import { Spacer } from "@nextui-org/react";
+
+import { Header } from "@/components/ui";
+
+export default async function Settings() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/ui/app/(prowler)/users/page.tsx b/ui/app/(prowler)/users/page.tsx
new file mode 100644
index 0000000000..364651ca22
--- /dev/null
+++ b/ui/app/(prowler)/users/page.tsx
@@ -0,0 +1,63 @@
+import { Spacer } from "@nextui-org/react";
+import { Suspense } from "react";
+
+import { getUsers } from "@/actions/users/users";
+import { FilterControls } from "@/components/filters";
+import { filterUsers } from "@/components/filters/data-filters";
+import { Header } from "@/components/ui";
+import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
+import { AddUserButton } from "@/components/users";
+import { ColumnsUser, SkeletonTableUser } from "@/components/users/table";
+import { SearchParamsProps } from "@/types";
+
+export default async function Users({
+ searchParams,
+}: {
+ searchParams: SearchParamsProps;
+}) {
+ const searchParamsKey = JSON.stringify(searchParams || {});
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ }>
+
+
+ >
+ );
+}
+
+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 usersData = await getUsers({ query, page, sort, filters });
+
+ return (
+
+ );
+};
diff --git a/ui/app/(prowler)/workloads/page.tsx b/ui/app/(prowler)/workloads/page.tsx
new file mode 100644
index 0000000000..3cadd0d1ca
--- /dev/null
+++ b/ui/app/(prowler)/workloads/page.tsx
@@ -0,0 +1,12 @@
+import { Spacer } from "@nextui-org/react";
+
+import { Header } from "@/components/ui";
+
+export default async function Workloads() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/ui/app/api/auth/[...nextauth]/route.ts b/ui/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000000..316cf49c14
--- /dev/null
+++ b/ui/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,3 @@
+import { handlers } from "@/auth.config";
+
+export const { GET, POST } = handlers;
diff --git a/ui/app/api/services/route.ts b/ui/app/api/services/route.ts
new file mode 100644
index 0000000000..dbd490c239
--- /dev/null
+++ b/ui/app/api/services/route.ts
@@ -0,0 +1,10 @@
+import { NextResponse } from "next/server";
+
+import data from "../../../dataServices.json";
+
+export async function GET() {
+ // Simulate fetching data with a delay
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
+ return NextResponse.json({ services: data });
+}
diff --git a/ui/app/providers.tsx b/ui/app/providers.tsx
new file mode 100644
index 0000000000..942bce43dc
--- /dev/null
+++ b/ui/app/providers.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import { NextUIProvider } from "@nextui-org/system";
+import { useRouter } from "next/navigation";
+import { SessionProvider } from "next-auth/react";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+import { ThemeProviderProps } from "next-themes/dist/types";
+import * as React from "react";
+
+export interface ProvidersProps {
+ children: React.ReactNode;
+ themeProps?: ThemeProviderProps;
+}
+
+export function Providers({ children, themeProps }: ProvidersProps) {
+ const router = useRouter();
+
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/ui/auth.config.ts b/ui/auth.config.ts
new file mode 100644
index 0000000000..56c44a14e5
--- /dev/null
+++ b/ui/auth.config.ts
@@ -0,0 +1,185 @@
+import { jwtDecode, JwtPayload } from "jwt-decode";
+import NextAuth, { type NextAuthConfig, User } from "next-auth";
+import Credentials from "next-auth/providers/credentials";
+import { z } from "zod";
+
+import { getToken, getUserByMe } from "./actions/auth";
+
+interface CustomJwtPayload extends JwtPayload {
+ user_id: string;
+ tenant_id: string;
+}
+
+const refreshAccessToken = async (token: JwtPayload) => {
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/tokens/refresh`);
+
+ const bodyData = {
+ data: {
+ type: "tokens-refresh",
+ attributes: {
+ refresh: (token as any).refreshToken,
+ },
+ },
+ };
+
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ },
+ body: JSON.stringify(bodyData),
+ });
+ const newTokens = await response.json();
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return {
+ ...token,
+ accessToken: newTokens.data.attributes.access,
+ refreshToken: newTokens.data.attributes.refresh,
+ };
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error("Error refreshing access token:", error);
+ return {
+ error: "RefreshAccessTokenError",
+ };
+ }
+};
+
+export const authConfig = {
+ session: {
+ strategy: "jwt",
+ // The session will be valid for 24 hours
+ maxAge: 24 * 60 * 60,
+ },
+ pages: {
+ signIn: "/sign-in",
+ newUser: "/sign-up",
+ },
+
+ providers: [
+ Credentials({
+ name: "credentials",
+ credentials: {
+ email: { label: "email", type: "text" },
+ password: { label: "password", type: "password" },
+ },
+ async authorize(credentials) {
+ const parsedCredentials = z
+ .object({
+ email: z.string().email(),
+ password: z.string().min(12),
+ })
+ .safeParse(credentials);
+
+ if (!parsedCredentials.success) return null;
+
+ const tokenResponse = await getToken(parsedCredentials.data);
+ if (!tokenResponse) return null;
+
+ const userMeResponse = await getUserByMe(tokenResponse.accessToken);
+
+ const user = {
+ name: userMeResponse.name,
+ email: userMeResponse.email,
+ company: userMeResponse?.company,
+ dateJoined: userMeResponse.dateJoined,
+ };
+
+ return {
+ ...user,
+ accessToken: tokenResponse.accessToken,
+ refreshToken: tokenResponse.refreshToken,
+ };
+ },
+ }),
+ ],
+ callbacks: {
+ authorized({ auth, request: { nextUrl } }) {
+ const isLoggedIn = !!auth?.user;
+ const isOnDashboard = nextUrl.pathname.startsWith("/");
+ const isSignUpPage = nextUrl.pathname === "/sign-up";
+
+ // Allow access to sign-up page
+ if (isSignUpPage) return true;
+
+ if (isOnDashboard) {
+ if (isLoggedIn) return true;
+ return false; // Redirect users who are not logged in to the login page
+ } else if (isLoggedIn) {
+ return Response.redirect(new URL("/", nextUrl));
+ }
+ return true;
+ },
+
+ jwt: async ({ token, account, user }) => {
+ if (token?.accessToken) {
+ const decodedToken = jwtDecode(
+ token.accessToken as string,
+ ) as CustomJwtPayload;
+ // eslint-disable-next-line no-console
+ // console.log("decodedToken", decodedToken);
+ token.accessTokenExpires = (decodedToken.exp as number) * 1000;
+ token.user_id = decodedToken.user_id;
+ token.tenant_id = decodedToken.tenant_id;
+ }
+
+ const userInfo = {
+ name: user?.name,
+ companyName: user?.company,
+ email: user?.email,
+ dateJoined: user?.dateJoined,
+ };
+
+ if (account && user) {
+ return {
+ ...token,
+ userId: token.user_id,
+ tenantId: token.tenant_id,
+ accessToken: (user as User & { accessToken: JwtPayload }).accessToken,
+ refreshToken: (user as User & { refreshToken: JwtPayload })
+ .refreshToken,
+ user: userInfo,
+ };
+ }
+
+ // eslint-disable-next-line no-console
+ // console.log(
+ // "Access token expires",
+ // token.accessTokenExpires,
+ // new Date(Number(token.accessTokenExpires)),
+ // );
+
+ // If the access token is not expired, return the token
+ if (
+ typeof token.accessTokenExpires === "number" &&
+ Date.now() < token.accessTokenExpires
+ )
+ return token;
+
+ // If the access token is expired, try to refresh it
+ return refreshAccessToken(token as JwtPayload);
+ },
+
+ session: async ({ session, token }) => {
+ if (token) {
+ session.userId = token?.user_id as string;
+ session.tenantId = token?.tenant_id as string;
+ session.accessToken = token?.accessToken as string;
+ session.refreshToken = token?.refreshToken as string;
+ session.user = token.user as any;
+ }
+
+ // console.log("session", session);
+ return session;
+ },
+ },
+} satisfies NextAuthConfig;
+
+export const { signIn, signOut, auth, handlers } = NextAuth(authConfig);
diff --git a/ui/components/ThemeSwitch.tsx b/ui/components/ThemeSwitch.tsx
new file mode 100644
index 0000000000..d397c4a678
--- /dev/null
+++ b/ui/components/ThemeSwitch.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import { SwitchProps, useSwitch } from "@nextui-org/react";
+import { useIsSSR } from "@react-aria/ssr";
+import { VisuallyHidden } from "@react-aria/visually-hidden";
+import clsx from "clsx";
+import { useTheme } from "next-themes";
+import { FC } from "react";
+import React from "react";
+
+import { MoonFilledIcon, SunFilledIcon } from "./icons";
+
+export interface ThemeSwitchProps {
+ className?: string;
+ classNames?: SwitchProps["classNames"];
+}
+
+export const ThemeSwitch: FC = ({
+ className,
+ classNames,
+}) => {
+ const { theme, setTheme } = useTheme();
+ const isSSR = useIsSSR();
+
+ const onChange = () => {
+ theme === "light" ? setTheme("dark") : setTheme("light");
+ };
+
+ const {
+ Component,
+ slots,
+ isSelected,
+ getBaseProps,
+ getInputProps,
+ getWrapperProps,
+ } = useSwitch({
+ isSelected: theme === "light" || isSSR,
+ "aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`,
+ onChange,
+ });
+
+ return (
+
+
+
+
+
+ {!isSelected || isSSR ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
diff --git a/ui/components/auth/oss/auth-form.tsx b/ui/components/auth/oss/auth-form.tsx
new file mode 100644
index 0000000000..1f1ec4384f
--- /dev/null
+++ b/ui/components/auth/oss/auth-form.tsx
@@ -0,0 +1,318 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Icon } from "@iconify/react";
+import { Button, Checkbox, Divider, Link } from "@nextui-org/react";
+import { useRouter } from "next/navigation";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { authenticate, createNewUser } from "@/actions/auth";
+import { NotificationIcon, ProwlerExtended } from "@/components/icons";
+import { ThemeSwitch } from "@/components/ThemeSwitch";
+import { useToast } from "@/components/ui";
+import { CustomButton, CustomInput } from "@/components/ui/custom";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormMessage,
+} from "@/components/ui/form";
+import { ApiError, authFormSchema } from "@/types";
+
+export const AuthForm = ({
+ type,
+ invitationToken,
+}: {
+ type: string;
+ invitationToken?: string | null;
+}) => {
+ const formSchema = authFormSchema(type);
+ const router = useRouter();
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: "",
+ password: "",
+ ...(type === "sign-up" && {
+ name: "",
+ company: "",
+ termsAndConditions: false,
+ confirmPassword: "",
+ ...(invitationToken && { invitationToken }),
+ }),
+ },
+ });
+
+ const isLoading = form.formState.isSubmitting;
+ const { toast } = useToast();
+
+ const onSubmit = async (data: z.infer) => {
+ if (type === "sign-in") {
+ const result = await authenticate(null, {
+ email: data.email.toLowerCase(),
+ password: data.password,
+ });
+
+ if (result?.message === "Success") {
+ router.push("/");
+ } else if (result?.errors && "credentials" in result.errors) {
+ form.setError("email", {
+ type: "server",
+ message: result.errors.credentials ?? "Incorrect email or password",
+ });
+ } else {
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: "An unexpected error occurred. Please try again.",
+ });
+ }
+ }
+
+ if (type === "sign-up") {
+ const newUser = await createNewUser(data);
+
+ if (!newUser.errors) {
+ toast({
+ title: "Success!",
+ description: "The user was registered successfully.",
+ });
+ form.reset();
+ router.push("/sign-in");
+ } else {
+ newUser.errors.forEach((error: ApiError) => {
+ const errorMessage = error.detail;
+ switch (error.source.pointer) {
+ case "/data/attributes/name":
+ form.setError("name", { type: "server", message: errorMessage });
+ break;
+ case "/data/attributes/email":
+ form.setError("email", { type: "server", message: errorMessage });
+ break;
+ case "/data/attributes/company_name":
+ form.setError("company", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/password":
+ form.setError("password", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data":
+ form.setError("invitationToken", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ default:
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ }
+ });
+ }
+ }
+ };
+
+ return (
+
+ {/* Auth Form */}
+
+ {/* Background Pattern */}
+
+
+
+ {/* Prowler Logo */}
+
+
+
+ {type === "sign-in" ? "Sign In" : "Sign Up"}
+
+
+
+
+
+
+
+ {type === "sign-in" && (
+ <>
+
+
+
+ }
+ variant="bordered"
+ >
+ Continue with Google
+
+
+ }
+ variant="bordered"
+ >
+ Continue with Github
+
+
+ >
+ )}
+ {type === "sign-in" ? (
+
+ Need to create an account?
+ Sign Up
+
+ ) : (
+
+ Already have an account?
+ Log In
+
+ )}
+
+
+
+ );
+};
diff --git a/ui/components/auth/oss/index.ts b/ui/components/auth/oss/index.ts
new file mode 100644
index 0000000000..82fc6feefa
--- /dev/null
+++ b/ui/components/auth/oss/index.ts
@@ -0,0 +1 @@
+export * from "./auth-form";
diff --git a/ui/components/charts/SeverityChart.tsx b/ui/components/charts/SeverityChart.tsx
new file mode 100644
index 0000000000..4c3060c8bd
--- /dev/null
+++ b/ui/components/charts/SeverityChart.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { Bar, BarChart, LabelList, XAxis, YAxis } from "recharts";
+
+import {
+ ChartConfig,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart/Chart";
+
+const chartData = [
+ { severity: "critical", findings: 32, fill: "var(--color-critical)" },
+ { severity: "high", findings: 78, fill: "var(--color-high)" },
+ { severity: "medium", findings: 117, fill: "var(--color-medium)" },
+ { severity: "low", findings: 39, fill: "var(--color-low)" },
+];
+
+const chartConfig = {
+ findings: {
+ label: "Findings",
+ },
+ critical: {
+ label: "Critical",
+ color: "hsl(var(--chart-critical))",
+ },
+ high: {
+ label: "High",
+ color: "hsl(var(--chart-fail))",
+ },
+ medium: {
+ label: "Medium",
+ color: "hsl(var(--chart-medium))",
+ },
+ low: {
+ label: "Low",
+ color: "hsl(var(--chart-low))",
+ },
+} satisfies ChartConfig;
+
+export const SeverityChart = () => {
+ return (
+
+
+
+
+ chartConfig[value as keyof typeof chartConfig]?.label
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+ );
+};
diff --git a/ui/components/charts/StatusChart.tsx b/ui/components/charts/StatusChart.tsx
new file mode 100644
index 0000000000..d906db502e
--- /dev/null
+++ b/ui/components/charts/StatusChart.tsx
@@ -0,0 +1,148 @@
+"use client";
+
+import { Chip, Divider, Spacer } from "@nextui-org/react";
+import { TrendingUp } from "lucide-react";
+import * as React from "react";
+import { Label, Pie, PieChart } from "recharts";
+
+import { NotificationIcon, SuccessIcon } from "../icons";
+import {
+ ChartConfig,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "../ui";
+
+const calculatePercent = (
+ chartData: { findings: string; number: number; fill: string }[],
+) => {
+ const total = chartData.reduce((sum, item) => sum + item.number, 0);
+
+ return chartData.map((item) => ({
+ ...item,
+ percent: Math.round((item.number / total) * 100) + "%",
+ }));
+};
+
+const chartData = [
+ {
+ findings: "Success",
+ number: 436,
+ fill: "var(--color-success)",
+ },
+ { findings: "Fail", number: 293, fill: "var(--color-fail)" },
+];
+
+const updatedChartData = calculatePercent(chartData);
+
+const chartConfig = {
+ number: {
+ label: "Findings",
+ },
+ success: {
+ label: "Success",
+ color: "hsl(var(--chart-success))",
+ },
+ fail: {
+ label: "Fail",
+ color: "hsl(var(--chart-fail))",
+ },
+} satisfies ChartConfig;
+
+export function StatusChart() {
+ const totalVisitors = React.useMemo(() => {
+ return chartData.reduce((acc, curr) => acc + curr.number, 0);
+ }, []);
+
+ return (
+
+
+
+ } />
+
+
+
+
+
+
+
}
+ color="success"
+ radius="lg"
+ size="md"
+ >
+ {chartData[0].number}
+
+
+
{updatedChartData[0].percent}
+
+
+
+ No change from last scan
+
+
+
+
+
}
+ color="danger"
+ radius="lg"
+ size="md"
+ >
+ {chartData[1].number}
+
+
+
{updatedChartData[1].percent}
+
+
+
+ +2 findings from last scan
+
+
+
+
+ );
+}
diff --git a/ui/components/charts/index.ts b/ui/components/charts/index.ts
new file mode 100644
index 0000000000..b4d3debb45
--- /dev/null
+++ b/ui/components/charts/index.ts
@@ -0,0 +1,2 @@
+export * from "./SeverityChart";
+export * from "./StatusChart";
diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx
new file mode 100644
index 0000000000..7fcfdbf5dd
--- /dev/null
+++ b/ui/components/compliance/compliance-card.tsx
@@ -0,0 +1,83 @@
+import { Card, CardBody, Progress } from "@nextui-org/react";
+import Image from "next/image";
+import React from "react";
+
+import { getComplianceIcon } from "../icons";
+
+interface ComplianceCardProps {
+ title: string;
+ passingRequirements: number;
+ totalRequirements: number;
+ prevPassingRequirements: number;
+ prevTotalRequirements: number;
+}
+
+export const ComplianceCard: React.FC = ({
+ title,
+ passingRequirements,
+ totalRequirements,
+}) => {
+ const ratingPercentage = Math.floor(
+ (passingRequirements / totalRequirements) * 100,
+ );
+
+ // const prevRatingPercentage = Math.floor(
+ // (prevPassingRequirements / prevTotalRequirements) * 100,
+ // );
+
+ // const getScanChange = () => {
+ // const scanDifference = ratingPercentage - prevRatingPercentage;
+ // if (scanDifference < 0 && scanDifference <= -1) {
+ // return `${scanDifference}% from last scan`;
+ // }
+ // if (scanDifference > 0 && scanDifference >= 1) {
+ // return `+${scanDifference}% from last scan`;
+ // }
+ // return "No changes from last scan";
+ // };
+
+ const getRatingColor = (ratingPercentage: number) => {
+ if (ratingPercentage <= 10) {
+ return "danger";
+ }
+ if (ratingPercentage <= 40) {
+ return "warning";
+ }
+ return "success";
+ };
+
+ return (
+
+
+
+
+
+
{title}
+
+
+
+
+ {passingRequirements} / {totalRequirements}
+
+ Passing Requirements
+
+ {/* {getScanChange()} */}
+
+
+
+
+
+ );
+};
diff --git a/ui/components/compliance/compliance-skeleton-grid.tsx b/ui/components/compliance/compliance-skeleton-grid.tsx
new file mode 100644
index 0000000000..54ec78ce26
--- /dev/null
+++ b/ui/components/compliance/compliance-skeleton-grid.tsx
@@ -0,0 +1,18 @@
+import { Card, Skeleton } from "@nextui-org/react";
+import React from "react";
+
+export const ComplianceSkeletonGrid = () => {
+ return (
+
+
+ {[...Array(28)].map((_, index) => (
+
+ ))}
+
+
+ );
+};
diff --git a/ui/components/compliance/data-compliance/data-compliance.tsx b/ui/components/compliance/data-compliance/data-compliance.tsx
new file mode 100644
index 0000000000..10fcc44f6b
--- /dev/null
+++ b/ui/components/compliance/data-compliance/data-compliance.tsx
@@ -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 (
+
+
+
+
+ {showClearButton && (
+ }
+ radius="sm"
+ >
+ Reset
+
+ )}
+
+
+ );
+};
diff --git a/ui/components/compliance/data-compliance/index.ts b/ui/components/compliance/data-compliance/index.ts
new file mode 100644
index 0000000000..14f430df04
--- /dev/null
+++ b/ui/components/compliance/data-compliance/index.ts
@@ -0,0 +1,2 @@
+export * from "./data-compliance";
+export * from "./select-scan-compliance-data";
diff --git a/ui/components/compliance/data-compliance/select-scan-compliance-data.tsx b/ui/components/compliance/data-compliance/select-scan-compliance-data.tsx
new file mode 100644
index 0000000000..5ebc36515d
--- /dev/null
+++ b/ui/components/compliance/data-compliance/select-scan-compliance-data.tsx
@@ -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 (
+
+ );
+};
diff --git a/ui/components/compliance/index.ts b/ui/components/compliance/index.ts
new file mode 100644
index 0000000000..bb1b06d995
--- /dev/null
+++ b/ui/components/compliance/index.ts
@@ -0,0 +1,2 @@
+export * from "./compliance-card";
+export * from "./compliance-skeleton-grid";
diff --git a/ui/components/filters/custom-account-selection.tsx b/ui/components/filters/custom-account-selection.tsx
new file mode 100644
index 0000000000..f9920a988f
--- /dev/null
+++ b/ui/components/filters/custom-account-selection.tsx
@@ -0,0 +1,32 @@
+"use client";
+import { Select, SelectItem } from "@nextui-org/react";
+
+const accounts = [
+ { key: "audit-test-1", label: "740350143844" },
+ { key: "audit-test-2", label: "890837126756" },
+ { key: "audit-test-3", label: "563829104923" },
+ { key: "audit-test-4", label: "678943217543" },
+ { key: "audit-test-5", label: "932187465320" },
+ { key: "audit-test-6", label: "492837106587" },
+ { key: "audit-test-7", label: "812736459201" },
+ { key: "audit-test-8", label: "374829106524" },
+ { key: "audit-test-9", label: "926481053298" },
+ { key: "audit-test-10", label: "748192364579" },
+ { key: "audit-test-11", label: "501374829106" },
+];
+export const CustomAccountSelection = () => {
+ return (
+
+ );
+};
diff --git a/ui/components/filters/custom-checkbox-muted-findings.tsx b/ui/components/filters/custom-checkbox-muted-findings.tsx
new file mode 100644
index 0000000000..a5e52b3d92
--- /dev/null
+++ b/ui/components/filters/custom-checkbox-muted-findings.tsx
@@ -0,0 +1,15 @@
+import { Checkbox } from "@nextui-org/react";
+import React from "react";
+
+export const CustomCheckboxMutedFindings = () => {
+ return (
+
+ Include Muted Findings
+
+ );
+};
diff --git a/ui/components/filters/custom-date-picker.tsx b/ui/components/filters/custom-date-picker.tsx
new file mode 100644
index 0000000000..2a982c1781
--- /dev/null
+++ b/ui/components/filters/custom-date-picker.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import {
+ getLocalTimeZone,
+ startOfMonth,
+ startOfWeek,
+ today,
+} from "@internationalized/date";
+import { Button, ButtonGroup, DatePicker } from "@nextui-org/react";
+import { useLocale } from "@react-aria/i18n";
+import { useRouter, useSearchParams } from "next/navigation";
+import React, { useCallback, useEffect, useRef } from "react";
+
+export const CustomDatePicker = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const defaultDate = today(getLocalTimeZone());
+
+ const [value, setValue] = React.useState(defaultDate);
+
+ const { locale } = useLocale();
+
+ const now = today(getLocalTimeZone());
+ const nextWeek = startOfWeek(now.add({ weeks: 1 }), locale);
+ const nextMonth = startOfMonth(now.add({ months: 1 }));
+
+ const applyDateFilter = useCallback(
+ (date: any) => {
+ const params = new URLSearchParams(searchParams.toString());
+ if (date) {
+ params.set("filter[updated_at]", date.toString());
+ } else {
+ params.delete("filter[updated_at]");
+ }
+ router.push(`?${params.toString()}`, { scroll: false });
+ },
+ [router, searchParams],
+ );
+
+ const initialRender = useRef(true);
+
+ useEffect(() => {
+ if (initialRender.current) {
+ initialRender.current = false;
+ return;
+ }
+ const params = new URLSearchParams(searchParams.toString());
+ if (params.size === 0) {
+ // If all params are cleared, reset to default date
+ setValue(defaultDate);
+ }
+ }, [searchParams]);
+
+ const handleDateChange = (newValue: any) => {
+ setValue(newValue);
+ applyDateFilter(newValue);
+ };
+
+ return (
+
+
+
+
+
+
+ }
+ calendarProps={{
+ focusedValue: value,
+ onFocusChange: setValue,
+ nextButtonProps: {
+ variant: "bordered",
+ },
+ prevButtonProps: {
+ variant: "bordered",
+ },
+ }}
+ value={value}
+ onChange={handleDateChange}
+ size="sm"
+ variant="flat"
+ />
+
+ );
+};
diff --git a/ui/components/filters/custom-provider-inputs.tsx b/ui/components/filters/custom-provider-inputs.tsx
new file mode 100644
index 0000000000..6587bcb7e7
--- /dev/null
+++ b/ui/components/filters/custom-provider-inputs.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+
+import {
+ AWSProviderBadge,
+ AzureProviderBadge,
+ GCPProviderBadge,
+ KS8ProviderBadge,
+} from "../icons/providers-badge";
+
+export const CustomProviderInputAWS = () => {
+ return (
+
+
+
Amazon Web Services
+
+ );
+};
+
+export const CustomProviderInputAzure = () => {
+ return (
+
+ );
+};
+
+export const CustomProviderInputGCP = () => {
+ return (
+
+
+
Google Cloud Platform
+
+ );
+};
+
+export const CustomProviderInputKubernetes = () => {
+ return (
+
+ );
+};
diff --git a/ui/components/filters/custom-region-selection.tsx b/ui/components/filters/custom-region-selection.tsx
new file mode 100644
index 0000000000..dd84cb4c1f
--- /dev/null
+++ b/ui/components/filters/custom-region-selection.tsx
@@ -0,0 +1,51 @@
+"use client";
+import { Select, SelectItem } from "@nextui-org/react";
+
+const regions = [
+ { key: "af-south-1", label: "AF South 1" },
+ { key: "ap-east-1", label: "AP East 1" },
+ { key: "ap-northeast-1", label: "AP Northeast 1" },
+ { key: "ap-northeast-2", label: "AP Northeast 2" },
+ { key: "ap-northeast-3", label: "AP Northeast 3" },
+ { key: "ap-south-1", label: "AP South 1" },
+ { key: "ap-south-2", label: "AP South 2" },
+ { key: "ap-southeast-1", label: "AP Southeast 1" },
+ { key: "ap-southeast-2", label: "AP Southeast 2" },
+ { key: "ap-southeast-3", label: "AP Southeast 3" },
+ { key: "ap-southeast-4", label: "AP Southeast 4" },
+ { key: "ca-central-1", label: "CA Central 1" },
+ { key: "ca-west-1", label: "CA West 1" },
+ { key: "eu-central-1", label: "EU Central 1" },
+ { key: "eu-central-2", label: "EU Central 2" },
+ { key: "eu-north-1", label: "EU North 1" },
+ { key: "eu-south-1", label: "EU South 1" },
+ { key: "eu-south-2", label: "EU South 2" },
+ { key: "eu-west-1", label: "EU West 1" },
+ { key: "eu-west-2", label: "EU West 2" },
+ { key: "eu-west-3", label: "EU West 3" },
+ { key: "il-central-1", label: "IL Central 1" },
+ { key: "me-central-1", label: "ME Central 1" },
+ { key: "me-south-1", label: "ME South 1" },
+ { key: "sa-east-1", label: "SA East 1" },
+ { key: "us-east-1", label: "US East 1" },
+ { key: "us-east-2", label: "US East 2" },
+ { key: "us-west-1", label: "US West 1" },
+ { key: "us-west-2", label: "US West 2" },
+];
+
+export const CustomRegionSelection = () => {
+ return (
+
+ );
+};
diff --git a/ui/components/filters/custom-search-input.tsx b/ui/components/filters/custom-search-input.tsx
new file mode 100644
index 0000000000..f3d9808125
--- /dev/null
+++ b/ui/components/filters/custom-search-input.tsx
@@ -0,0 +1,62 @@
+import { Input } from "@nextui-org/react";
+import debounce from "lodash.debounce";
+import { SearchIcon, XCircle } from "lucide-react";
+import { useRouter, useSearchParams } from "next/navigation";
+import React, { useCallback, useEffect, useState } from "react";
+
+export const CustomSearchInput: React.FC = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const [searchQuery, setSearchQuery] = useState("");
+
+ const applySearch = useCallback(
+ (query: string) => {
+ const params = new URLSearchParams(searchParams.toString());
+ if (query) {
+ params.set("filter[search]", query);
+ } else {
+ params.delete("filter[search]");
+ }
+ router.push(`?${params.toString()}`, { scroll: false });
+ },
+ [router, searchParams],
+ );
+
+ const debouncedChangeHandler = useCallback(debounce(applySearch, 300), []);
+
+ const clearIconSearch = () => {
+ setSearchQuery("");
+ applySearch("");
+ };
+
+ useEffect(() => {
+ const searchFromUrl = searchParams.get("filter[search]") || "";
+ setSearchQuery(searchFromUrl);
+ }, [searchParams]);
+
+ return (
+ }
+ onChange={(e) => {
+ const value = e.target.value;
+ setSearchQuery(value);
+ debouncedChangeHandler(value);
+ }}
+ endContent={
+ searchQuery && (
+
+ )
+ }
+ radius="sm"
+ size="sm"
+ />
+ );
+};
diff --git a/ui/components/filters/custom-select-provider.tsx b/ui/components/filters/custom-select-provider.tsx
new file mode 100644
index 0000000000..d2a419bbbb
--- /dev/null
+++ b/ui/components/filters/custom-select-provider.tsx
@@ -0,0 +1,91 @@
+"use client";
+
+import { Select, SelectItem } from "@nextui-org/react";
+import { useRouter, useSearchParams } from "next/navigation";
+import React, { useCallback, useMemo } from "react";
+
+import {
+ CustomProviderInputAWS,
+ CustomProviderInputAzure,
+ CustomProviderInputGCP,
+ CustomProviderInputKubernetes,
+} from "./custom-provider-inputs";
+
+const dataInputsProvider = [
+ {
+ key: "aws",
+ label: "Amazon Web Services",
+ value: ,
+ },
+ {
+ key: "gcp",
+ label: "Google Cloud Platform",
+ value: ,
+ },
+ {
+ key: "azure",
+ label: "Microsoft Azure",
+ value: ,
+ },
+ {
+ key: "kubernetes",
+ label: "Kubernetes",
+ value: ,
+ },
+];
+
+export const CustomSelectProvider: React.FC = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const applyProviderFilter = useCallback(
+ (value: string) => {
+ const params = new URLSearchParams(searchParams.toString());
+ if (value) {
+ params.set("filter[provider_type]", value);
+ } else {
+ params.delete("filter[provider_type]");
+ }
+ router.push(`?${params.toString()}`, { scroll: false });
+ },
+ [router, searchParams],
+ );
+
+ const currentProvider = searchParams.get("filter[provider_type]") || "";
+
+ const selectedKeys = useMemo(() => {
+ return dataInputsProvider.some(
+ (provider) => provider.key === currentProvider,
+ )
+ ? [currentProvider]
+ : [];
+ }, [currentProvider]);
+
+ return (
+
+ );
+};
diff --git a/ui/components/filters/data-filters.ts b/ui/components/filters/data-filters.ts
new file mode 100644
index 0000000000..e396791b64
--- /dev/null
+++ b/ui/components/filters/data-filters.ts
@@ -0,0 +1,74 @@
+export const filterProviders = [
+ {
+ key: "connected",
+ labelCheckboxGroup: "Connection",
+ values: ["false", "true"],
+ },
+ // Add more filter categories as needed
+];
+
+export const filterScans = [
+ {
+ key: "provider_type__in",
+ labelCheckboxGroup: "Provider",
+ values: ["aws", "azure", "gcp", "kubernetes"],
+ },
+ {
+ key: "state",
+ labelCheckboxGroup: "State",
+ values: [
+ "available",
+ "scheduled",
+ "executing",
+ "completed",
+ "failed",
+ "cancelled",
+ ],
+ },
+ {
+ key: "trigger",
+ labelCheckboxGroup: "Schedule",
+ values: ["scheduled", "manual"],
+ },
+ // Add more filter categories as needed
+];
+
+export const filterFindings = [
+ {
+ key: "severity__in",
+ labelCheckboxGroup: "Severity",
+ values: ["critical", "high", "medium", "low", "informational"],
+ },
+ {
+ key: "status__in",
+ labelCheckboxGroup: "Status",
+ values: ["PASS", "FAIL", "MANUAL", "MUTED"],
+ },
+ {
+ key: "delta__in",
+ labelCheckboxGroup: "Delta",
+ values: ["new", "changed"],
+ },
+ {
+ key: "provider_type__in",
+ labelCheckboxGroup: "Provider",
+ values: ["aws", "azure", "gcp", "kubernetes"],
+ },
+ // Add more filter categories as needed
+];
+
+export const filterUsers = [
+ {
+ key: "is_active",
+ labelCheckboxGroup: "Status",
+ values: ["true", "false"],
+ },
+];
+
+export const filterInvitations = [
+ {
+ key: "state",
+ labelCheckboxGroup: "State",
+ values: ["pending", "accepted", "expired", "revoked"],
+ },
+];
diff --git a/ui/components/filters/filter-controls.tsx b/ui/components/filters/filter-controls.tsx
new file mode 100644
index 0000000000..6f08b5c928
--- /dev/null
+++ b/ui/components/filters/filter-controls.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import { useRouter, useSearchParams } from "next/navigation";
+import React, { useCallback, useEffect, useState } from "react";
+
+import { FilterControlsProps } from "@/types";
+
+import { CrossIcon } from "../icons";
+import { CustomButton } from "../ui/custom";
+import { DataTableFilterCustom } from "../ui/table";
+import { CustomAccountSelection } from "./custom-account-selection";
+import { CustomCheckboxMutedFindings } from "./custom-checkbox-muted-findings";
+import { CustomDatePicker } from "./custom-date-picker";
+import { CustomRegionSelection } from "./custom-region-selection";
+import { CustomSearchInput } from "./custom-search-input";
+import { CustomSelectProvider } from "./custom-select-provider";
+
+export const FilterControls: React.FC = ({
+ search = false,
+ providers = false,
+ date = false,
+ regions = false,
+ accounts = false,
+ mutedFindings = false,
+ customFilters,
+}) => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const [showClearButton, setShowClearButton] = useState(false);
+
+ useEffect(() => {
+ const hasFilters = Array.from(searchParams.keys()).some(
+ (key) => key.startsWith("filter[") || key === "sort",
+ );
+ setShowClearButton(hasFilters);
+ }, [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 (
+
+
+ {search && }
+ {providers && }
+ {date && }
+ {regions && }
+ {accounts && }
+ {mutedFindings && }
+
+ {showClearButton && (
+ }
+ radius="sm"
+ >
+ Reset
+
+ )}
+
+ {customFilters &&
}
+
+ );
+};
diff --git a/ui/components/filters/index.ts b/ui/components/filters/index.ts
new file mode 100644
index 0000000000..915bbd3aec
--- /dev/null
+++ b/ui/components/filters/index.ts
@@ -0,0 +1,8 @@
+export * from "./custom-account-selection";
+export * from "./custom-checkbox-muted-findings";
+export * from "./custom-date-picker";
+export * from "./custom-provider-inputs";
+export * from "./custom-region-selection";
+export * from "./custom-select-provider";
+export * from "./data-filters";
+export * from "./filter-controls";
diff --git a/ui/components/findings/table/column-findings.tsx b/ui/components/findings/table/column-findings.tsx
new file mode 100644
index 0000000000..d5c06862db
--- /dev/null
+++ b/ui/components/findings/table/column-findings.tsx
@@ -0,0 +1,177 @@
+"use client";
+
+import { ColumnDef } from "@tanstack/react-table";
+import { useSearchParams } from "next/navigation";
+
+import { DataTableRowDetails } from "@/components/findings/table";
+import { InfoIcon } from "@/components/icons";
+import { TriggerSheet } from "@/components/ui/sheet";
+import {
+ DataTableColumnHeader,
+ SeverityBadge,
+ StatusFindingBadge,
+} from "@/components/ui/table";
+import { FindingProps } from "@/types";
+
+import { DataTableRowActions } from "./data-table-row-actions";
+
+const getFindingsData = (row: { original: FindingProps }) => {
+ 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: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const { checktitle } = getFindingsMetadata(row);
+ return {checktitle}
;
+ },
+ },
+ {
+ accessorKey: "severity",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { severity },
+ } = getFindingsData(row);
+ return ;
+ },
+ },
+ {
+ accessorKey: "status",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { status },
+ } = getFindingsData(row);
+
+ return ;
+ },
+ },
+ {
+ accessorKey: "scanName",
+ header: "Scan Name",
+ cell: ({ row }) => {
+ const name = getScanData(row, "name");
+
+ return (
+
+ {typeof name === "string" || typeof name === "number"
+ ? name
+ : "Invalid data"}
+
+ );
+ },
+ },
+ {
+ 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: "moreInfo",
+ header: "Details",
+ cell: ({ row }) => {
+ const searchParams = useSearchParams();
+ const findingId = searchParams.get("id");
+ const isOpen = findingId === row.original.id;
+ return (
+
+ }
+ title="Finding Details"
+ description="View the finding details"
+ defaultOpen={isOpen}
+ >
+
+
+
+ );
+ },
+ },
+ {
+ id: "actions",
+ cell: ({ row }) => {
+ return ;
+ },
+ },
+];
diff --git a/ui/components/findings/table/data-table-row-actions.tsx b/ui/components/findings/table/data-table-row-actions.tsx
new file mode 100644
index 0000000000..8e15c942a0
--- /dev/null
+++ b/ui/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,
+ 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 findingId = (row.original as { id: string }).id;
+ return (
+ <>
+ {/*
+
+
+
+
+ */}
+
+
+
+
+
+
+
+
+ }
+ // onClick={() => setIsEditOpen(true)}
+ >
+ {findingId}
+ Send to Jira
+
+ }
+ // onClick={() => setIsEditOpen(true)}
+ >
+ Send to Slack
+
+
+
+
+
+ >
+ );
+}
diff --git a/ui/components/findings/table/data-table-row-details.tsx b/ui/components/findings/table/data-table-row-details.tsx
new file mode 100644
index 0000000000..9f43d8b165
--- /dev/null
+++ b/ui/components/findings/table/data-table-row-details.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+// import { usePathname, useRouter, useSearchParams } from "next/navigation";
+// import { useEffect } from "react";
+
+import { FindingProps } from "@/types/components";
+
+import { FindingDetail } from "./finding-detail";
+
+export const DataTableRowDetails = ({
+ // entityId,
+ findingDetails,
+}: {
+ entityId: string;
+ findingDetails: FindingProps;
+}) => {
+ // const router = useRouter();
+ // const pathname = usePathname();
+ // const searchParams = useSearchParams();
+
+ // useEffect(() => {
+ // if (entityId) {
+ // const params = new URLSearchParams(searchParams.toString());
+ // params.set("id", entityId);
+ // router.push(`${pathname}?${params.toString()}`, { scroll: false });
+ // }
+
+ // return () => {
+ // if (entityId) {
+ // const cleanupParams = new URLSearchParams(searchParams.toString());
+ // cleanupParams.delete("id");
+ // router.push(`${pathname}?${cleanupParams.toString()}`, {
+ // scroll: false,
+ // });
+ // }
+ // };
+ // }, [entityId, pathname, router, searchParams]);
+
+ return ;
+};
diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx
new file mode 100644
index 0000000000..7f2a84e714
--- /dev/null
+++ b/ui/components/findings/table/finding-detail.tsx
@@ -0,0 +1,219 @@
+"use client";
+
+import { Snippet } from "@nextui-org/react";
+import Link from "next/link";
+
+import { SnippetId } from "@/components/ui/entities";
+import { DateWithTime } from "@/components/ui/entities/date-with-time";
+import { SeverityBadge } from "@/components/ui/table/severity-badge";
+import { FindingProps } from "@/types";
+
+export const FindingDetail = ({
+ findingDetails,
+}: {
+ findingDetails: FindingProps;
+}) => {
+ const finding = findingDetails;
+ const attributes = finding.attributes;
+ const resource = finding.relationships.resource.attributes;
+
+ const remediation = attributes.check_metadata.remediation;
+
+ return (
+
+ {/* Header */}
+
+
+
+ {attributes.check_metadata.checktitle}
+
+
+ {resource.service}
+
+
+
+ {attributes.status}
+
+
+
+ {/* Check Metadata */}
+
+
+
+ Check Metadata
+
+
+
+ {attributes.status === "FAIL" && (
+
+
+ Risk
+
+
+ {attributes.check_metadata.risk}
+
+
+ )}
+
+
+
+ Description
+
+
+ {attributes.check_metadata.description}
+
+
+
+
+
+ Remediation
+
+
+ {remediation.recommendation && (
+ <>
+
Recommendation:
+
{remediation.recommendation.text}
+
+ Learn more
+
+ >
+ )}
+ {remediation.code &&
+ Object.values(remediation.code).some(Boolean) && (
+
+
+ Check these links:
+
+
+ {remediation.code.cli && (
+
+
CLI Command:
+
+
+ {remediation.code.cli}
+
+
+
+ )}
+
+ {Object.entries(remediation.code)
+ .filter(([key]) => key !== "cli")
+ .map(([key, value]) =>
+ value ? (
+
+ {key === "other"
+ ? "External doc"
+ : key.charAt(0).toUpperCase() +
+ key.slice(1).toLowerCase()}
+
+ ) : null,
+ )}
+
+
+
+ )}
+
+
+
+
+ {/* Resources Section */}
+
+
+ Resource Details
+
+
+
+
+ Resource ID
+
+
+ {resource.uid}
+
+
+
+
+ Resource Name
+
+
+ {resource.name}
+
+
+
+
+ Region
+
+
+ {resource.region}
+
+
+
+
+ Resource Type
+
+
+ {resource.type}
+
+
+
+ {resource.tags &&
+ Object.entries(resource.tags).map(([key, value]) => (
+
+
+ Tag: {key}
+
+
+ {value}
+
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/ui/components/findings/table/index.ts b/ui/components/findings/table/index.ts
new file mode 100644
index 0000000000..fe19b5dd6e
--- /dev/null
+++ b/ui/components/findings/table/index.ts
@@ -0,0 +1,5 @@
+export * from "./column-findings";
+export * from "./data-table-row-actions";
+export * from "./data-table-row-details";
+export * from "./finding-detail";
+export * from "./skeleton-table-findings";
diff --git a/ui/components/findings/table/skeleton-table-findings.tsx b/ui/components/findings/table/skeleton-table-findings.tsx
new file mode 100644
index 0000000000..3af6403e7c
--- /dev/null
+++ b/ui/components/findings/table/skeleton-table-findings.tsx
@@ -0,0 +1,65 @@
+import { Card, Skeleton } from "@nextui-org/react";
+import React from "react";
+
+export const SkeletonTableFindings = () => {
+ return (
+
+ {/* Table headers */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Table body */}
+
+ {[...Array(3)].map((_, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/ui/components/icons/Icons.tsx b/ui/components/icons/Icons.tsx
new file mode 100644
index 0000000000..b8f448afd3
--- /dev/null
+++ b/ui/components/icons/Icons.tsx
@@ -0,0 +1,810 @@
+import * as React from "react";
+
+import { IconSvgProps } from "@/types";
+
+export const TwitterIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const GithubIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const MoonFilledIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const SunFilledIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const SearchIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const ChevronDownIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ strokeWidth = 1.5,
+ ...props
+}) => (
+
+);
+
+export const PlusIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const VerticalDotsIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const DeleteIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const CheckIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const CrossIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const PassIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const RocketIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const AlertIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const NotificationIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const IdIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const DoneIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const CopyIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const FlowIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const ConnectionIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const ConnectionTrue: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const ConnectionFalse: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const ConnectionPending: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const SuccessIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
+
+export const ArrowUpIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const ArrowDownIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const ChevronsLeftRightIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const PlusCircleIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const CustomFilterIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const SaveIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const AddIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const ScheduleIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const InfoIcon: React.FC = ({
+ size = 24,
+ width,
+ height,
+ ...props
+}) => (
+
+);
diff --git a/ui/components/icons/compliance/IconCompliance.tsx b/ui/components/icons/compliance/IconCompliance.tsx
new file mode 100644
index 0000000000..543f6c68c8
--- /dev/null
+++ b/ui/components/icons/compliance/IconCompliance.tsx
@@ -0,0 +1,63 @@
+import AWSLogo from "./aws.svg";
+import CISLogo from "./cis.svg";
+import CISALogo from "./cisa.svg";
+import ENSLogo from "./ens.png";
+import FedRAMPLogo from "./fedramp.svg";
+import FFIECLogo from "./ffiec.svg";
+import GDPRLogo from "./gdpr.svg";
+import GxPLogo from "./gxp-aws.svg";
+import HIPAALogo from "./hipaa.svg";
+import ISOLogo from "./iso-27001.svg";
+import MITRELogo from "./mitre-attack.svg";
+import NISTLogo from "./nist.svg";
+import PCILogo from "./pci-dss.svg";
+import RBILogo from "./rbi.svg";
+import SOC2Logo from "./soc2.svg";
+
+export const getComplianceIcon = (complianceTitle: string) => {
+ if (complianceTitle.toLowerCase().includes("aws")) {
+ return AWSLogo;
+ }
+ if (complianceTitle.toLowerCase().includes("cisa")) {
+ return CISALogo;
+ }
+ if (complianceTitle.toLowerCase().includes("cis")) {
+ return CISLogo;
+ }
+ if (complianceTitle.toLowerCase().includes("ens")) {
+ return ENSLogo;
+ }
+ if (complianceTitle.toLowerCase().includes("ffiec")) {
+ return FFIECLogo;
+ }
+ if (complianceTitle.toLowerCase().includes("fedramp")) {
+ return FedRAMPLogo;
+ }
+ if (complianceTitle.toLowerCase().includes("gdpr")) {
+ return GDPRLogo;
+ }
+ if (complianceTitle.toLowerCase().includes("gxp")) {
+ return GxPLogo;
+ }
+ if (complianceTitle.toLowerCase().includes("hipaa")) {
+ return HIPAALogo;
+ }
+ if (complianceTitle.toLowerCase().includes("iso")) {
+ return ISOLogo;
+ }
+ if (complianceTitle.toLowerCase().includes("mitre")) {
+ return MITRELogo;
+ }
+ if (complianceTitle.toLowerCase().includes("nist")) {
+ return NISTLogo;
+ }
+ if (complianceTitle.toLowerCase().includes("pci")) {
+ return PCILogo;
+ }
+ if (complianceTitle.toLowerCase().includes("rbi")) {
+ return RBILogo;
+ }
+ if (complianceTitle.toLowerCase().includes("soc2")) {
+ return SOC2Logo;
+ }
+};
diff --git a/ui/components/icons/compliance/aws.svg b/ui/components/icons/compliance/aws.svg
new file mode 100644
index 0000000000..4715937ff0
--- /dev/null
+++ b/ui/components/icons/compliance/aws.svg
@@ -0,0 +1,38 @@
+
+
+
diff --git a/ui/components/icons/compliance/cis.svg b/ui/components/icons/compliance/cis.svg
new file mode 100644
index 0000000000..013f35a75e
--- /dev/null
+++ b/ui/components/icons/compliance/cis.svg
@@ -0,0 +1,9 @@
+
diff --git a/ui/components/icons/compliance/cisa.svg b/ui/components/icons/compliance/cisa.svg
new file mode 100644
index 0000000000..c26f681d51
--- /dev/null
+++ b/ui/components/icons/compliance/cisa.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/ui/components/icons/compliance/ens.png b/ui/components/icons/compliance/ens.png
new file mode 100644
index 0000000000..c3e6433f31
Binary files /dev/null and b/ui/components/icons/compliance/ens.png differ
diff --git a/ui/components/icons/compliance/fedramp.svg b/ui/components/icons/compliance/fedramp.svg
new file mode 100644
index 0000000000..7706bd4eff
--- /dev/null
+++ b/ui/components/icons/compliance/fedramp.svg
@@ -0,0 +1,9 @@
+
diff --git a/ui/components/icons/compliance/ffiec.svg b/ui/components/icons/compliance/ffiec.svg
new file mode 100644
index 0000000000..40951851e9
--- /dev/null
+++ b/ui/components/icons/compliance/ffiec.svg
@@ -0,0 +1,9 @@
+
diff --git a/ui/components/icons/compliance/gdpr.svg b/ui/components/icons/compliance/gdpr.svg
new file mode 100644
index 0000000000..31175e9afe
--- /dev/null
+++ b/ui/components/icons/compliance/gdpr.svg
@@ -0,0 +1,9 @@
+
diff --git a/ui/components/icons/compliance/gxp-aws.svg b/ui/components/icons/compliance/gxp-aws.svg
new file mode 100644
index 0000000000..7b22b53de3
--- /dev/null
+++ b/ui/components/icons/compliance/gxp-aws.svg
@@ -0,0 +1,455 @@
+
+
+
diff --git a/ui/components/icons/compliance/hipaa.svg b/ui/components/icons/compliance/hipaa.svg
new file mode 100644
index 0000000000..b9537032f6
--- /dev/null
+++ b/ui/components/icons/compliance/hipaa.svg
@@ -0,0 +1,9 @@
+
diff --git a/ui/components/icons/compliance/iso-27001.svg b/ui/components/icons/compliance/iso-27001.svg
new file mode 100644
index 0000000000..109c19521d
--- /dev/null
+++ b/ui/components/icons/compliance/iso-27001.svg
@@ -0,0 +1,9 @@
+
diff --git a/ui/components/icons/compliance/mitre-attack.svg b/ui/components/icons/compliance/mitre-attack.svg
new file mode 100644
index 0000000000..c546eedd83
--- /dev/null
+++ b/ui/components/icons/compliance/mitre-attack.svg
@@ -0,0 +1,315 @@
+
+
+
diff --git a/ui/components/icons/compliance/nist.svg b/ui/components/icons/compliance/nist.svg
new file mode 100644
index 0000000000..4231017c94
--- /dev/null
+++ b/ui/components/icons/compliance/nist.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/components/icons/compliance/pci-dss.svg b/ui/components/icons/compliance/pci-dss.svg
new file mode 100644
index 0000000000..52f93be38b
--- /dev/null
+++ b/ui/components/icons/compliance/pci-dss.svg
@@ -0,0 +1,9 @@
+
diff --git a/ui/components/icons/compliance/rbi.svg b/ui/components/icons/compliance/rbi.svg
new file mode 100644
index 0000000000..ce1d81f83d
--- /dev/null
+++ b/ui/components/icons/compliance/rbi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/components/icons/compliance/soc2.svg b/ui/components/icons/compliance/soc2.svg
new file mode 100644
index 0000000000..d2b8782541
--- /dev/null
+++ b/ui/components/icons/compliance/soc2.svg
@@ -0,0 +1,9 @@
+
diff --git a/ui/components/icons/index.ts b/ui/components/icons/index.ts
new file mode 100644
index 0000000000..7912d466fd
--- /dev/null
+++ b/ui/components/icons/index.ts
@@ -0,0 +1,4 @@
+export * from "./compliance/IconCompliance";
+export * from "./Icons";
+export * from "./prowler/ProwlerIcons";
+export * from "./services/IconServices";
diff --git a/ui/components/icons/providers-badge/AWSProviderBadge.tsx b/ui/components/icons/providers-badge/AWSProviderBadge.tsx
new file mode 100644
index 0000000000..f0dc91958e
--- /dev/null
+++ b/ui/components/icons/providers-badge/AWSProviderBadge.tsx
@@ -0,0 +1,42 @@
+import * as React from "react";
+
+import { IconSvgProps } from "@/types";
+
+export const AWSProviderBadge: React.FC = ({
+ size,
+ width,
+ height,
+ ...props
+}) => (
+
+);
diff --git a/ui/components/icons/providers-badge/AzureProviderBadge.tsx b/ui/components/icons/providers-badge/AzureProviderBadge.tsx
new file mode 100644
index 0000000000..983e4e41df
--- /dev/null
+++ b/ui/components/icons/providers-badge/AzureProviderBadge.tsx
@@ -0,0 +1,80 @@
+import * as React from "react";
+
+import { IconSvgProps } from "@/types";
+
+export const AzureProviderBadge: React.FC = ({
+ size,
+ width,
+ height,
+ ...props
+}) => (
+
+);
diff --git a/ui/components/icons/providers-badge/GCPProviderBadge.tsx b/ui/components/icons/providers-badge/GCPProviderBadge.tsx
new file mode 100644
index 0000000000..987a9fd074
--- /dev/null
+++ b/ui/components/icons/providers-badge/GCPProviderBadge.tsx
@@ -0,0 +1,42 @@
+import * as React from "react";
+
+import { IconSvgProps } from "@/types";
+
+export const GCPProviderBadge: React.FC = ({
+ size,
+ width,
+ height,
+ ...props
+}) => (
+
+);
diff --git a/ui/components/icons/providers-badge/KS8ProviderBadge.tsx b/ui/components/icons/providers-badge/KS8ProviderBadge.tsx
new file mode 100644
index 0000000000..280b4d381c
--- /dev/null
+++ b/ui/components/icons/providers-badge/KS8ProviderBadge.tsx
@@ -0,0 +1,32 @@
+import * as React from "react";
+
+import { IconSvgProps } from "@/types";
+
+export const KS8ProviderBadge: React.FC = ({
+ size,
+ width,
+ height,
+ ...props
+}) => (
+
+);
diff --git a/ui/components/icons/providers-badge/index.ts b/ui/components/icons/providers-badge/index.ts
new file mode 100644
index 0000000000..8aa68424a0
--- /dev/null
+++ b/ui/components/icons/providers-badge/index.ts
@@ -0,0 +1,4 @@
+export * from "./AWSProviderBadge";
+export * from "./AzureProviderBadge";
+export * from "./GCPProviderBadge";
+export * from "./KS8ProviderBadge";
diff --git a/ui/components/icons/prowler/ProwlerIcons.tsx b/ui/components/icons/prowler/ProwlerIcons.tsx
new file mode 100644
index 0000000000..ae0b989e6b
--- /dev/null
+++ b/ui/components/icons/prowler/ProwlerIcons.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+
+import { IconSvgProps } from "../../../types/index";
+
+export const ProwlerExtended: React.FC = ({
+ size,
+ width = 216,
+ height,
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export const ProwlerShort: React.FC = ({
+ size,
+ width = 30,
+ height,
+ ...props
+}) => (
+
+);
diff --git a/ui/components/icons/services/IconServices.tsx b/ui/components/icons/services/IconServices.tsx
new file mode 100644
index 0000000000..2678b3ef7a
--- /dev/null
+++ b/ui/components/icons/services/IconServices.tsx
@@ -0,0 +1,923 @@
+import { IconSvgProps } from "@/types";
+
+export const IAMAccessAnalyzerIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSAccountIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSCertificateManagerIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSAthenaIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSLambdaIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSCloudFormationIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSCloudTrailIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSCloudWatchIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSConfigIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSDatabaseMigrationServiceIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AmazonEC2Icon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AmazonEMRIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSGlueIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AmazonGuardDutyIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSIAMIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AmazonInspectorIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AmazonMacieIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSNetworkFirewallIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSOrganizationsIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AmazonRDSIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSResourceExplorerIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AmazonRoute53Icon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AmazonS3Icon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSSecurityHubIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AmazonSNSIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSSystemsManagerIncidentManagerIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AWSTrustedAdvisorIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const AmazonVPCIcon: React.FC = ({
+ size = 32,
+ width,
+ height,
+ className = "rounded-md",
+ ...props
+}) => (
+
+);
+
+export const getAWSIcon = (serviceAlias: string) => {
+ switch (serviceAlias) {
+ case "Amazon EC2":
+ return ;
+ case "Amazon EMR":
+ return ;
+ case "Amazon GuardDuty":
+ return ;
+ case "Amazon Inspector":
+ return ;
+ case "Amazon Macie":
+ return ;
+ case "Amazon RDS":
+ return ;
+ case "Amazon Route 53":
+ return ;
+ case "Amazon S3":
+ return ;
+ case "Amazon SNS":
+ return ;
+ case "Amazon VPC":
+ return ;
+ case "AWS Account":
+ return ;
+ case "AWS Athena":
+ return ;
+ case "AWS Certificate Manager":
+ return ;
+ case "AWS CloudFormation":
+ return ;
+ case "AWS CloudTrail":
+ return ;
+ case "AWS CloudWatch":
+ return ;
+ case "AWS Config":
+ return ;
+ case "AWS Database Migration":
+ return ;
+ case "AWS Glue":
+ return ;
+ case "AWS IAM":
+ return ;
+ case "AWS Lambda":
+ return ;
+ case "AWS Network Firewall":
+ return ;
+ case "AWS Organizations":
+ return ;
+ case "AWS Resource Explorer":
+ return ;
+ case "AWS Security Hub":
+ return ;
+ case "AWS Systems Manager":
+ return ;
+ case "AWS Trusted Advisor":
+ return ;
+ case "IAM Access Analyzer":
+ return ;
+ default:
+ return null;
+ }
+};
diff --git a/ui/components/invitations/forms/delete-form.tsx b/ui/components/invitations/forms/delete-form.tsx
new file mode 100644
index 0000000000..bea018b2ae
--- /dev/null
+++ b/ui/components/invitations/forms/delete-form.tsx
@@ -0,0 +1,95 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import React, { Dispatch, SetStateAction } from "react";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { revokeInvite } from "@/actions/invitations/invitation";
+import { DeleteIcon } from "@/components/icons";
+import { useToast } from "@/components/ui";
+import { CustomButton } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+
+const formSchema = z.object({
+ invitationId: z.string(),
+});
+
+export const DeleteForm = ({
+ invitationId,
+ setIsOpen,
+}: {
+ invitationId: string;
+ setIsOpen: Dispatch>;
+}) => {
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ invitationId,
+ },
+ });
+ const { toast } = useToast();
+ const isLoading = form.formState.isSubmitting;
+
+ async function onSubmitClient(values: z.infer) {
+ const formData = new FormData();
+
+ Object.entries(values).forEach(
+ ([key, value]) => value !== undefined && formData.append(key, value),
+ );
+ // client-side validation
+ const data = await revokeInvite(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ const error = data.errors[0];
+ const errorMessage = `${error.detail}`;
+ // show error
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ } else {
+ toast({
+ title: "Success!",
+ description: "The invitation was revoked successfully.",
+ });
+ }
+ setIsOpen(false); // Close the modal on success
+ }
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/invitations/forms/edit-form.tsx b/ui/components/invitations/forms/edit-form.tsx
new file mode 100644
index 0000000000..17924cea97
--- /dev/null
+++ b/ui/components/invitations/forms/edit-form.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Dispatch, SetStateAction } from "react";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { updateInvite } from "@/actions/invitations/invitation";
+import { SaveIcon } from "@/components/icons";
+import { useToast } from "@/components/ui";
+import { CustomButton, CustomInput } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import { editInviteFormSchema } from "@/types";
+
+export const EditForm = ({
+ invitationId,
+ invitationEmail,
+ setIsOpen,
+}: {
+ invitationId: string;
+ invitationEmail?: string;
+ setIsOpen: Dispatch>;
+}) => {
+ const formSchema = editInviteFormSchema;
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ invitationId,
+ invitationEmail: invitationEmail,
+ },
+ });
+
+ const { toast } = useToast();
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: z.infer) => {
+ const formData = new FormData();
+ console.log(values);
+
+ Object.entries(values).forEach(
+ ([key, value]) => value !== undefined && formData.append(key, value),
+ );
+
+ const data = await updateInvite(formData);
+
+ if (data?.error) {
+ const errorMessage = `${data.error}`;
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ } else {
+ toast({
+ title: "Success!",
+ description: "The invitation was updated successfully.",
+ });
+ setIsOpen(false); // Close the modal on success
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/invitations/forms/index.ts b/ui/components/invitations/forms/index.ts
new file mode 100644
index 0000000000..a081952ade
--- /dev/null
+++ b/ui/components/invitations/forms/index.ts
@@ -0,0 +1,2 @@
+export * from "./delete-form";
+export * from "./edit-form";
diff --git a/ui/components/invitations/index.ts b/ui/components/invitations/index.ts
new file mode 100644
index 0000000000..e381a114df
--- /dev/null
+++ b/ui/components/invitations/index.ts
@@ -0,0 +1,2 @@
+export * from "./invitation-details";
+export * from "./send-invitation-button";
diff --git a/ui/components/invitations/invitation-details.tsx b/ui/components/invitations/invitation-details.tsx
new file mode 100644
index 0000000000..f335ad6487
--- /dev/null
+++ b/ui/components/invitations/invitation-details.tsx
@@ -0,0 +1,111 @@
+"use client";
+
+import { Card, CardBody, Divider, Snippet } from "@nextui-org/react";
+
+import { AddIcon } from "../icons";
+import { CustomButton } from "../ui/custom";
+import { DateWithTime } from "../ui/entities";
+
+interface InvitationDetailsProps {
+ attributes: {
+ email: string;
+ state: string;
+ token: string;
+ expires_at: string;
+ inserted_at: string;
+ updated_at: string;
+ };
+ relationships?: {
+ inviter: {
+ data: {
+ id: string;
+ };
+ };
+ };
+ selfLink: string;
+}
+
+export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => {
+ const baseURL = process.env.SITE_URL || "http://localhost:3000";
+ const invitationLink = `${baseURL}/sign-up?invitation_token=${attributes.token}`;
+ return (
+
+
+
+
+ Invitation Details
+
+
+
+
+
+ Email:
+ {attributes.email}
+
+
+
+ State:
+ {attributes.state}
+
+
+
+ Token:
+ {attributes.token}
+
+
+
+ Expires At:
+
+
+
+
+ Inserted At:
+
+
+
+
+ Updated At:
+
+
+
+
+
+
+ Share this link with the user:
+
+
+
+
+
+ {invitationLink}
+
+
+
+
+
+
+ }
+ >
+ Back to Invitations
+
+
+
+ );
+};
diff --git a/ui/components/invitations/send-invitation-button.tsx b/ui/components/invitations/send-invitation-button.tsx
new file mode 100644
index 0000000000..ae13d33b1d
--- /dev/null
+++ b/ui/components/invitations/send-invitation-button.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { AddIcon } from "../icons";
+import { CustomButton } from "../ui/custom";
+
+export const SendInvitationButton = () => {
+ return (
+
+ }
+ >
+ Send Invitation
+
+
+ );
+};
diff --git a/ui/components/invitations/table/column-invitations.tsx b/ui/components/invitations/table/column-invitations.tsx
new file mode 100644
index 0000000000..9e30eb8024
--- /dev/null
+++ b/ui/components/invitations/table/column-invitations.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import { ColumnDef } from "@tanstack/react-table";
+
+import { DateWithTime } from "@/components/ui/entities";
+import { DataTableColumnHeader } from "@/components/ui/table";
+import { InvitationProps } from "@/types";
+
+import { DataTableRowActions } from "./data-table-row-actions";
+
+const getInvitationData = (row: { original: InvitationProps }) => {
+ return row.original.attributes;
+};
+
+export const ColumnsInvitation: ColumnDef[] = [
+ {
+ accessorKey: "email",
+ header: () => Email
,
+ cell: ({ row }) => {
+ const data = getInvitationData(row);
+ return {data?.email || "N/A"}
;
+ },
+ },
+ {
+ accessorKey: "state",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const { state } = getInvitationData(row);
+ return {state}
;
+ },
+ },
+ {
+ accessorKey: "inserted_at",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const { inserted_at } = getInvitationData(row);
+ return ;
+ },
+ },
+
+ {
+ accessorKey: "expires_at",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const { expires_at } = getInvitationData(row);
+ return ;
+ },
+ },
+
+ {
+ accessorKey: "actions",
+ header: () => Actions
,
+ id: "actions",
+ cell: ({ row }) => {
+ return ;
+ },
+ },
+];
diff --git a/ui/components/invitations/table/data-table-row-actions.tsx b/ui/components/invitations/table/data-table-row-actions.tsx
new file mode 100644
index 0000000000..6f60d2e0b7
--- /dev/null
+++ b/ui/components/invitations/table/data-table-row-actions.tsx
@@ -0,0 +1,120 @@
+"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 clsx from "clsx";
+import { useState } from "react";
+
+import { VerticalDotsIcon } from "@/components/icons";
+import { CustomAlertModal } from "@/components/ui/custom";
+
+import { DeleteForm, EditForm } from "../forms";
+
+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 invitationId = (row.original as { id: string }).id;
+ const invitationEmail = (row.original as any).attributes?.email;
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+ Check Details
+
+
+ }
+ onClick={() => setIsEditOpen(true)}
+ >
+ Edit Invitation
+
+
+
+
+ }
+ onClick={() => setIsDeleteOpen(true)}
+ >
+ Revoke Invitation
+
+
+
+
+
+ >
+ );
+}
diff --git a/ui/components/invitations/table/index.ts b/ui/components/invitations/table/index.ts
new file mode 100644
index 0000000000..177edf2bb9
--- /dev/null
+++ b/ui/components/invitations/table/index.ts
@@ -0,0 +1,3 @@
+export * from "./column-invitations";
+export * from "./data-table-row-actions";
+export * from "./skeleton-table-invitations";
diff --git a/ui/components/invitations/table/skeleton-table-invitations.tsx b/ui/components/invitations/table/skeleton-table-invitations.tsx
new file mode 100644
index 0000000000..6d18313fe4
--- /dev/null
+++ b/ui/components/invitations/table/skeleton-table-invitations.tsx
@@ -0,0 +1,59 @@
+import { Card, Skeleton } from "@nextui-org/react";
+import React from "react";
+
+export const SkeletonTableInvitation = () => {
+ return (
+
+ {/* Table headers */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Table body */}
+
+ {[...Array(10)].map((_, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/ui/components/invitations/workflow/forms/index.ts b/ui/components/invitations/workflow/forms/index.ts
new file mode 100644
index 0000000000..3180c95b40
--- /dev/null
+++ b/ui/components/invitations/workflow/forms/index.ts
@@ -0,0 +1 @@
+export * from "./send-invitation-form";
diff --git a/ui/components/invitations/workflow/forms/send-invitation-form.tsx b/ui/components/invitations/workflow/forms/send-invitation-form.tsx
new file mode 100644
index 0000000000..a75c6a60c6
--- /dev/null
+++ b/ui/components/invitations/workflow/forms/send-invitation-form.tsx
@@ -0,0 +1,107 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { SaveIcon } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { sendInvite } from "@/actions/invitations/invitation";
+import { useToast } from "@/components/ui";
+import { CustomButton, CustomInput } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import { ApiError } from "@/types";
+
+const sendInvitationFormSchema = z.object({
+ email: z.string().email("Please enter a valid email"),
+});
+
+export type FormValues = z.infer;
+
+export const SendInvitationForm = () => {
+ const { toast } = useToast();
+ const router = useRouter();
+
+ const form = useForm({
+ resolver: zodResolver(sendInvitationFormSchema),
+ defaultValues: {
+ email: "",
+ },
+ });
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormValues) => {
+ const formData = new FormData();
+ formData.append("email", values.email);
+
+ try {
+ const data = await sendInvite(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ data.errors.forEach((error: ApiError) => {
+ const errorMessage = error.detail;
+ switch (error.source.pointer) {
+ case "/data/attributes/email":
+ form.setError("email", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ default:
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ }
+ });
+ } else {
+ const invitationId = data?.data?.id || "";
+ router.push(`/invitations/check-details/?id=${invitationId}`);
+ }
+ } catch (error) {
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: "An unexpected error occurred. Please try again.",
+ });
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/invitations/workflow/index.ts b/ui/components/invitations/workflow/index.ts
new file mode 100644
index 0000000000..a9a860f213
--- /dev/null
+++ b/ui/components/invitations/workflow/index.ts
@@ -0,0 +1,3 @@
+export * from "./skeleton-invitation-info";
+export * from "./vertical-steps";
+export * from "./workflow-send-invite";
diff --git a/ui/components/invitations/workflow/skeleton-invitation-info.tsx b/ui/components/invitations/workflow/skeleton-invitation-info.tsx
new file mode 100644
index 0000000000..12abeb9da7
--- /dev/null
+++ b/ui/components/invitations/workflow/skeleton-invitation-info.tsx
@@ -0,0 +1,65 @@
+import { Card, Skeleton } from "@nextui-org/react";
+import React from "react";
+
+export const SkeletonInvitationInfo = () => {
+ return (
+
+ {/* Table headers */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Table body */}
+
+ {[...Array(3)].map((_, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/ui/components/invitations/workflow/vertical-steps.tsx b/ui/components/invitations/workflow/vertical-steps.tsx
new file mode 100644
index 0000000000..74eaa32747
--- /dev/null
+++ b/ui/components/invitations/workflow/vertical-steps.tsx
@@ -0,0 +1,291 @@
+"use client";
+
+import type { ButtonProps } from "@nextui-org/react";
+import { cn } from "@nextui-org/react";
+import { useControlledState } from "@react-stately/utils";
+import { domAnimation, LazyMotion, m } from "framer-motion";
+import type { ComponentProps } from "react";
+import React from "react";
+
+export type VerticalStepProps = {
+ className?: string;
+ description?: React.ReactNode;
+ title?: React.ReactNode;
+};
+
+export interface VerticalStepsProps
+ extends React.HTMLAttributes {
+ /**
+ * An array of steps.
+ *
+ * @default []
+ */
+ steps?: VerticalStepProps[];
+ /**
+ * The color of the steps.
+ *
+ * @default "primary"
+ */
+ color?: ButtonProps["color"];
+ /**
+ * The current step index.
+ */
+ currentStep?: number;
+ /**
+ * The default step index.
+ *
+ * @default 0
+ */
+ defaultStep?: number;
+ /**
+ * Whether to hide the progress bars.
+ *
+ * @default false
+ */
+ hideProgressBars?: boolean;
+ /**
+ * The custom class for the steps wrapper.
+ */
+ className?: string;
+ /**
+ * The custom class for the step.
+ */
+ stepClassName?: string;
+ /**
+ * Callback function when the step index changes.
+ */
+ onStepChange?: (stepIndex: number) => void;
+}
+
+function CheckIcon(props: ComponentProps<"svg">) {
+ return (
+
+ );
+}
+
+export const VerticalSteps = React.forwardRef<
+ HTMLButtonElement,
+ VerticalStepsProps
+>(
+ (
+ {
+ color = "primary",
+ steps = [],
+ defaultStep = 0,
+ onStepChange,
+ currentStep: currentStepProp,
+ hideProgressBars = false,
+ stepClassName,
+ className,
+ ...props
+ },
+ ref,
+ ) => {
+ const [currentStep, setCurrentStep] = useControlledState(
+ currentStepProp,
+ defaultStep,
+ onStepChange,
+ );
+
+ const colors = React.useMemo(() => {
+ let userColor;
+ let fgColor;
+
+ const colorsVars = [
+ "[--active-fg-color:var(--step-fg-color)]",
+ "[--active-border-color:var(--step-color)]",
+ "[--active-color:var(--step-color)]",
+ "[--complete-background-color:var(--step-color)]",
+ "[--complete-border-color:var(--step-color)]",
+ "[--inactive-border-color:hsl(var(--nextui-default-300))]",
+ "[--inactive-color:hsl(var(--nextui-default-300))]",
+ ];
+
+ switch (color) {
+ case "primary":
+ userColor = "[--step-color:hsl(var(--nextui-primary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-primary-foreground))]";
+ break;
+ case "secondary":
+ userColor = "[--step-color:hsl(var(--nextui-secondary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-secondary-foreground))]";
+ break;
+ case "success":
+ userColor = "[--step-color:hsl(var(--nextui-success))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-success-foreground))]";
+ break;
+ case "warning":
+ userColor = "[--step-color:hsl(var(--nextui-warning))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-warning-foreground))]";
+ break;
+ case "danger":
+ userColor = "[--step-color:hsl(var(--nextui-error))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-error-foreground))]";
+ break;
+ case "default":
+ userColor = "[--step-color:hsl(var(--nextui-default))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-default-foreground))]";
+ break;
+ default:
+ userColor = "[--step-color:hsl(var(--nextui-primary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-primary-foreground))]";
+ break;
+ }
+
+ if (!className?.includes("--step-fg-color")) colorsVars.unshift(fgColor);
+ if (!className?.includes("--step-color")) colorsVars.unshift(userColor);
+ if (!className?.includes("--inactive-bar-color"))
+ colorsVars.push(
+ "[--inactive-bar-color:hsl(var(--nextui-default-300))]",
+ );
+
+ return colorsVars;
+ }, [color, className]);
+
+ return (
+
+ );
+ },
+);
+
+VerticalSteps.displayName = "VerticalSteps";
diff --git a/ui/components/invitations/workflow/workflow-send-invite.tsx b/ui/components/invitations/workflow/workflow-send-invite.tsx
new file mode 100644
index 0000000000..9cbe848093
--- /dev/null
+++ b/ui/components/invitations/workflow/workflow-send-invite.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { Progress, Spacer } from "@nextui-org/react";
+import { usePathname } from "next/navigation";
+import React from "react";
+
+import { VerticalSteps } from "./vertical-steps";
+
+const steps = [
+ {
+ title: "Send Invitation",
+ description:
+ "Enter the email address of the person you want to invite and send the invitation.",
+ href: "/invitations/new",
+ },
+ {
+ title: "Review Invitation Details",
+ description:
+ "Review the invitation details and share the information required for the person to accept the invitation.",
+ href: "/invitations/check-details",
+ },
+];
+
+export const WorkflowSendInvite = () => {
+ const pathname = usePathname();
+
+ // Calculate current step based on pathname
+ const currentStepIndex = steps.findIndex((step) =>
+ pathname.endsWith(step.href),
+ );
+ const currentStep = currentStepIndex === -1 ? 0 : currentStepIndex;
+
+ return (
+
+
+ Send invitation
+
+
+ Follow the steps to send an invitation to the users.
+
+
+
+
+
+ );
+};
diff --git a/ui/components/overview/AttackSurface.tsx b/ui/components/overview/AttackSurface.tsx
new file mode 100644
index 0000000000..495ceb3d1c
--- /dev/null
+++ b/ui/components/overview/AttackSurface.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+
+import { ActionCard } from "@/components/ui";
+
+const cardData = [
+ {
+ findings: 3,
+ title: "Internet Exposed Resources",
+ },
+ {
+ findings: 15,
+ title: "Exposed Secrets",
+ },
+ {
+ findings: 0,
+ title: "IAM Policies Leading to Privilege Escalation",
+ },
+ {
+ findings: 0,
+ 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"
+ : "heroicons:shield-check-solid"
+ }
+ title={card.title}
+ description={
+ card.findings > 0 ? "Review Required" : "No Issues Found"
+ }
+ />
+ ))}
+
+ );
+};
diff --git a/ui/components/overview/index.ts b/ui/components/overview/index.ts
new file mode 100644
index 0000000000..eb393767f1
--- /dev/null
+++ b/ui/components/overview/index.ts
@@ -0,0 +1,3 @@
+export * from "./AttackSurface";
+export * from "./provider-overview/provider-overview";
+export * from "./provider-overview/skeleton-provider-overview";
diff --git a/ui/components/overview/provider-overview/provider-overview.tsx b/ui/components/overview/provider-overview/provider-overview.tsx
new file mode 100644
index 0000000000..cd69f02595
--- /dev/null
+++ b/ui/components/overview/provider-overview/provider-overview.tsx
@@ -0,0 +1,150 @@
+"use client";
+
+import { Card, CardBody, CardHeader } from "@nextui-org/react";
+
+import { AddIcon } from "@/components/icons/Icons";
+import {
+ AWSProviderBadge,
+ AzureProviderBadge,
+ GCPProviderBadge,
+ KS8ProviderBadge,
+} from "@/components/icons/providers-badge";
+import { CustomButton } from "@/components/ui/custom/custom-button";
+import { ProviderOverviewProps } from "@/types";
+
+export const ProvidersOverview = ({
+ providersOverview,
+}: {
+ providersOverview: ProviderOverviewProps;
+}) => {
+ console.log(providersOverview);
+ if (!providersOverview || !Array.isArray(providersOverview.data)) {
+ return No provider data available
;
+ }
+
+ const calculatePassingPercentage = (pass: number, total: number) =>
+ total > 0 ? ((pass / total) * 100).toFixed(2) : "0.00";
+
+ const renderProviderBadge = (providerId: string) => {
+ switch (providerId) {
+ case "aws":
+ return ;
+ case "azure":
+ return ;
+ case "gcp":
+ return ;
+ case "kubernetes":
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ const providers = [
+ { id: "aws", name: "AWS" },
+ { id: "azure", name: "Azure" },
+ { id: "gcp", name: "GCP" },
+ { id: "kubernetes", name: "Kubernetes" },
+ ];
+
+ return (
+
+
+ Providers Overview
+
+
+
+
+ Provider
+
+ Percent
+ Passing
+
+
+ Failing
+ Checks
+
+
+ Total
+ Resources
+
+
+
+ {providers.map((providerTemplate) => {
+ const providerData = providersOverview.data.find(
+ (p) => p.id === providerTemplate.id,
+ );
+
+ return (
+
+
+ {renderProviderBadge(providerTemplate.id)}
+
+
+ {providerData
+ ? calculatePassingPercentage(
+ providerData.attributes.findings.pass,
+ providerData.attributes.findings.total,
+ )
+ : "0.00"}
+ %
+
+
+ {providerData ? providerData.attributes.findings.fail : "-"}
+
+
+ {providerData ? providerData.attributes.resources.total : "-"}
+
+
+ );
+ })}
+
+ {/* Totals row */}
+
+ Total
+
+ {calculatePassingPercentage(
+ providersOverview.data.reduce(
+ (sum, provider) => sum + provider.attributes.findings.pass,
+ 0,
+ ),
+ providersOverview.data.reduce(
+ (sum, provider) => sum + provider.attributes.findings.total,
+ 0,
+ ),
+ )}
+ %
+
+
+ {providersOverview.data.reduce(
+ (sum, provider) => sum + provider.attributes.findings.fail,
+ 0,
+ )}
+
+
+ {providersOverview.data.reduce(
+ (sum, provider) => sum + provider.attributes.resources.total,
+ 0,
+ )}
+
+
+
+
+ }
+ >
+ Add Provider
+
+
+
+
+ );
+};
diff --git a/ui/components/overview/provider-overview/skeleton-provider-overview.tsx b/ui/components/overview/provider-overview/skeleton-provider-overview.tsx
new file mode 100644
index 0000000000..47febd1700
--- /dev/null
+++ b/ui/components/overview/provider-overview/skeleton-provider-overview.tsx
@@ -0,0 +1,64 @@
+import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react";
+
+export const SkeletonProvidersOverview = () => {
+ const rows = 4;
+
+ return (
+
+
+
+
+
+
+
+
+ {/* Header Skeleton */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Row Skeletons */}
+ {Array.from({ length: rows }).map((_, index) => (
+
+ {/* Provider Name */}
+
+ {/* Percent Passing */}
+
+
+
+ {/* Failing Checks */}
+
+
+
+ {/* Total Resources */}
+
+
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/ui/components/primitives.ts b/ui/components/primitives.ts
new file mode 100644
index 0000000000..472973cbe9
--- /dev/null
+++ b/ui/components/primitives.ts
@@ -0,0 +1,53 @@
+import { tv } from "tailwind-variants";
+
+export const title = tv({
+ base: "tracking-tight inline font-semibold",
+ variants: {
+ color: {
+ violet: "from-[#FF1CF7] to-[#b249f8]",
+ yellow: "from-[#FF705B] to-[#FFB457]",
+ blue: "from-[#5EA2EF] to-[#0072F5]",
+ cyan: "from-[#00b7fa] to-[#01cfea]",
+ green: "from-[#6FEE8D] to-[#17c964]",
+ pink: "from-[#FF72E1] to-[#F54C7A]",
+ foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]",
+ },
+ size: {
+ sm: "text-3xl lg:text-4xl",
+ md: "text-[2.3rem] lg:text-5xl leading-9",
+ lg: "text-4xl lg:text-6xl",
+ },
+ fullWidth: {
+ true: "w-full block",
+ },
+ },
+ defaultVariants: {
+ size: "md",
+ },
+ compoundVariants: [
+ {
+ color: [
+ "violet",
+ "yellow",
+ "blue",
+ "cyan",
+ "green",
+ "pink",
+ "foreground",
+ ],
+ class: "bg-clip-text text-transparent bg-gradient-to-b",
+ },
+ ],
+});
+
+export const subtitle = tv({
+ base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full",
+ variants: {
+ fullWidth: {
+ true: "!w-full",
+ },
+ },
+ defaultVariants: {
+ fullWidth: true,
+ },
+});
diff --git a/ui/components/providers/add-provider.tsx b/ui/components/providers/add-provider.tsx
new file mode 100644
index 0000000000..c732d41007
--- /dev/null
+++ b/ui/components/providers/add-provider.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { AddIcon } from "../icons";
+import { CustomButton } from "../ui/custom";
+
+export const AddProvider = () => {
+ return (
+
+ }
+ >
+ Add Account
+
+
+ );
+};
diff --git a/ui/components/providers/forms/delete-form.tsx b/ui/components/providers/forms/delete-form.tsx
new file mode 100644
index 0000000000..2f80b3294b
--- /dev/null
+++ b/ui/components/providers/forms/delete-form.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import React, { Dispatch, SetStateAction } from "react";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { deleteProvider } from "@/actions/providers";
+import { DeleteIcon } from "@/components/icons";
+import { useToast } from "@/components/ui";
+import { CustomButton } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+
+const formSchema = z.object({
+ providerId: z.string(),
+});
+
+export const DeleteForm = ({
+ providerId,
+ setIsOpen,
+}: {
+ providerId: string;
+ setIsOpen: Dispatch>;
+}) => {
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ });
+ const { toast } = useToast();
+ const isLoading = form.formState.isSubmitting;
+
+ async function onSubmitClient(formData: FormData) {
+ // client-side validation
+ const data = await deleteProvider(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ const error = data.errors[0];
+ const errorMessage = `${error.detail}`;
+ // show error
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ } else {
+ toast({
+ title: "Success!",
+ description: "The provider was removed successfully.",
+ });
+ }
+ setIsOpen(false); // Close the modal on success
+ }
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/providers/forms/edit-form.tsx b/ui/components/providers/forms/edit-form.tsx
new file mode 100644
index 0000000000..fba187a22f
--- /dev/null
+++ b/ui/components/providers/forms/edit-form.tsx
@@ -0,0 +1,119 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Dispatch, SetStateAction } from "react";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { updateProvider } from "@/actions/providers";
+import { SaveIcon } from "@/components/icons";
+import { useToast } from "@/components/ui";
+import { CustomButton, CustomInput } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import { editProviderFormSchema } from "@/types";
+
+export const EditForm = ({
+ providerId,
+ providerAlias,
+ setIsOpen,
+}: {
+ providerId: string;
+ providerAlias?: string;
+ setIsOpen: Dispatch>;
+}) => {
+ const formSchema = editProviderFormSchema(providerAlias ?? "");
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerId: providerId,
+ alias: providerAlias,
+ },
+ });
+
+ const { toast } = useToast();
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: z.infer) => {
+ const formData = new FormData();
+
+ Object.entries(values).forEach(
+ ([key, value]) => value !== undefined && formData.append(key, value),
+ );
+
+ const data = await updateProvider(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ const error = data.errors[0];
+ const errorMessage = `${error.detail}`;
+ // show error
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ } else {
+ toast({
+ title: "Success!",
+ description: "The provider was updated successfully.",
+ });
+ setIsOpen(false); // Close the modal on success
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/providers/forms/index.ts b/ui/components/providers/forms/index.ts
new file mode 100644
index 0000000000..a081952ade
--- /dev/null
+++ b/ui/components/providers/forms/index.ts
@@ -0,0 +1,2 @@
+export * from "./delete-form";
+export * from "./edit-form";
diff --git a/ui/components/providers/index.ts b/ui/components/providers/index.ts
new file mode 100644
index 0000000000..6fb3bcae31
--- /dev/null
+++ b/ui/components/providers/index.ts
@@ -0,0 +1,4 @@
+export * from "./add-provider";
+export * from "./forms/delete-form";
+export * from "./provider-info";
+export * from "./radio-group-provider";
diff --git a/ui/components/providers/provider-info.tsx b/ui/components/providers/provider-info.tsx
new file mode 100644
index 0000000000..9d6a11c371
--- /dev/null
+++ b/ui/components/providers/provider-info.tsx
@@ -0,0 +1,78 @@
+import React from "react";
+
+import { ConnectionFalse, ConnectionPending, ConnectionTrue } from "../icons";
+import {
+ AWSProviderBadge,
+ AzureProviderBadge,
+ GCPProviderBadge,
+ KS8ProviderBadge,
+} from "../icons/providers-badge";
+
+interface ProviderInfoProps {
+ connected: boolean | null;
+ provider: "aws" | "azure" | "gcp" | "kubernetes";
+ providerAlias: string;
+}
+
+export const ProviderInfo: React.FC = ({
+ connected,
+ provider,
+ providerAlias,
+}) => {
+ const getIcon = () => {
+ switch (connected) {
+ case true:
+ return (
+
+
+
+ );
+ case false:
+ return (
+
+
+
+ );
+ case null:
+ return (
+
+
+
+ );
+ default:
+ return ;
+ }
+ };
+
+ const getProviderLogo = () => {
+ switch (provider) {
+ case "aws":
+ return ;
+ case "azure":
+ return ;
+ case "gcp":
+ return ;
+ case "kubernetes":
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+
+
{getProviderLogo()}
+
{getIcon()}
+
+
+ {providerAlias}
+
+ {/* */}
+
+
+
+
+ );
+};
diff --git a/ui/components/providers/radio-group-provider.tsx b/ui/components/providers/radio-group-provider.tsx
new file mode 100644
index 0000000000..e376e69ff4
--- /dev/null
+++ b/ui/components/providers/radio-group-provider.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import { RadioGroup } from "@nextui-org/react";
+import React from "react";
+import { Control, Controller } from "react-hook-form";
+import { z } from "zod";
+
+import { addProviderFormSchema } from "@/types";
+
+import { AWSProviderBadge, AzureProviderBadge } from "../icons/providers-badge";
+import { GCPProviderBadge } from "../icons/providers-badge/GCPProviderBadge";
+import { KS8ProviderBadge } from "../icons/providers-badge/KS8ProviderBadge";
+import { CustomRadio } from "../ui/custom";
+import { FormMessage } from "../ui/form";
+
+interface RadioGroupProviderProps {
+ control: Control>;
+ isInvalid: boolean;
+ errorMessage?: string;
+}
+
+export const RadioGroupProvider: React.FC = ({
+ control,
+ isInvalid,
+ errorMessage,
+}) => {
+ return (
+ (
+ <>
+
+
+
+
+
+
Amazon Web Services
+
+
+
+
+
+ Google Cloud Platform
+
+
+
+
+
+
+
+
+ Kubernetes
+
+
+
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+ >
+ )}
+ />
+ );
+};
diff --git a/ui/components/providers/table/column-providers.tsx b/ui/components/providers/table/column-providers.tsx
new file mode 100644
index 0000000000..1680fd8000
--- /dev/null
+++ b/ui/components/providers/table/column-providers.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import { ColumnDef } from "@tanstack/react-table";
+
+import { DateWithTime, SnippetId } from "@/components/ui/entities";
+import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table";
+import { ProviderProps } from "@/types";
+
+import { ProviderInfo } from "../provider-info";
+import { DataTableRowActions } from "./data-table-row-actions";
+
+const getProviderData = (row: { original: ProviderProps }) => {
+ return row.original;
+};
+
+export const ColumnProviders: ColumnDef[] = [
+ // {
+ // header: " ",
+ // cell: ({ row }) => {row.index + 1}
,
+ // },
+ {
+ accessorKey: "account",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { connection, provider, alias },
+ } = getProviderData(row);
+ return (
+
+ );
+ },
+ },
+ {
+ accessorKey: "uid",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { uid },
+ } = getProviderData(row);
+ return ;
+ },
+ },
+ {
+ accessorKey: "status",
+ header: "Scan Status",
+ cell: () => {
+ // Temporarily overwriting the value until the API is functional.
+ return ;
+ },
+ },
+ {
+ accessorKey: "lastScan",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { updated_at },
+ } = getProviderData(row);
+ return ;
+ },
+ },
+ {
+ accessorKey: "added",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { inserted_at },
+ } = getProviderData(row);
+ return ;
+ },
+ },
+ {
+ id: "actions",
+ cell: ({ row }) => {
+ return ;
+ },
+ },
+];
diff --git a/ui/components/providers/table/data-table-row-actions.tsx b/ui/components/providers/table/data-table-row-actions.tsx
new file mode 100644
index 0000000000..29d0bc127e
--- /dev/null
+++ b/ui/components/providers/table/data-table-row-actions.tsx
@@ -0,0 +1,121 @@
+"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 clsx from "clsx";
+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 providerType = (row.original as any).attributes?.provider;
+ const providerAlias = (row.original as any).attributes?.alias;
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+ Test Connection
+
+ }
+ onClick={() => setIsEditOpen(true)}
+ >
+ Edit Provider
+
+
+
+
+ }
+ onClick={() => setIsDeleteOpen(true)}
+ >
+ Delete Provider
+
+
+
+
+
+ >
+ );
+}
diff --git a/ui/components/providers/table/index.ts b/ui/components/providers/table/index.ts
new file mode 100644
index 0000000000..eb6e772b49
--- /dev/null
+++ b/ui/components/providers/table/index.ts
@@ -0,0 +1,3 @@
+export * from "./column-providers";
+export * from "./data-table-row-actions";
+export * from "./skeleton-table-provider";
diff --git a/ui/components/providers/table/skeleton-table-provider.tsx b/ui/components/providers/table/skeleton-table-provider.tsx
new file mode 100644
index 0000000000..b5c852d55e
--- /dev/null
+++ b/ui/components/providers/table/skeleton-table-provider.tsx
@@ -0,0 +1,65 @@
+import { Card, Skeleton } from "@nextui-org/react";
+import React from "react";
+
+export const SkeletonTableProviders = () => {
+ return (
+
+ {/* Table headers */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Table body */}
+
+ {[...Array(3)].map((_, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/ui/components/providers/workflow/forms/connect-account-form.tsx b/ui/components/providers/workflow/forms/connect-account-form.tsx
new file mode 100644
index 0000000000..54acff1491
--- /dev/null
+++ b/ui/components/providers/workflow/forms/connect-account-form.tsx
@@ -0,0 +1,226 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { ChevronLeftIcon, ChevronRightIcon, SaveIcon } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { useToast } from "@/components/ui";
+import { CustomButton, CustomInput } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+
+import { addProvider } from "../../../../actions/providers/providers";
+import { addProviderFormSchema, ApiError } from "../../../../types";
+import { RadioGroupProvider } from "../../radio-group-provider";
+import { RadioGroupAWSViaCredentialsForm } from "./radio-group-aws-via-credentials-form";
+
+export type FormValues = z.infer;
+
+export const ConnectAccountForm = () => {
+ const { toast } = useToast();
+ const [prevStep, setPrevStep] = useState(1);
+ const router = useRouter();
+
+ const formSchema = addProviderFormSchema;
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerType: undefined,
+ providerUid: "",
+ providerAlias: "",
+ awsCredentialsType: "",
+ },
+ });
+ const providerType = form.watch("providerType");
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormValues) => {
+ const formValues = { ...values };
+
+ // If providerAlias is empty, set default value
+ if (!formValues.providerAlias.trim()) {
+ const date = new Date();
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
+ const day = date.getDate().toString().padStart(2, "0");
+ const year = date.getFullYear();
+ formValues.providerAlias = `${formValues.providerType}:${month}/${day}/${year}`;
+ }
+
+ const formData = new FormData();
+
+ Object.entries(formValues).forEach(
+ ([key, value]) => value !== undefined && formData.append(key, value),
+ );
+
+ const data = await addProvider(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ data.errors.forEach((error: ApiError) => {
+ const errorMessage = error.detail;
+ const pointer = error.source?.pointer;
+
+ switch (pointer) {
+ case "/data/attributes/provider":
+ form.setError("providerType", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/uid":
+ case "/data/attributes/__all__":
+ form.setError("providerUid", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/alias":
+ form.setError("providerAlias", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ default:
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ }
+ });
+ setPrevStep(1);
+ } else {
+ const {
+ id,
+ attributes: { provider: providerType },
+ } = data.data;
+ const credentialsParam = values.awsCredentialsType
+ ? `&via=${values.awsCredentialsType}`
+ : "";
+ router.push(
+ `/providers/add-credentials?type=${providerType}&id=${id}${credentialsParam}`,
+ );
+ }
+ };
+
+ const handleNextStep = () => {
+ setPrevStep((prev) => prev + 1);
+ };
+
+ const handleBackStep = () => {
+ setPrevStep((prev) => prev - 1);
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/providers/workflow/forms/index.ts b/ui/components/providers/workflow/forms/index.ts
new file mode 100644
index 0000000000..be1bb3277d
--- /dev/null
+++ b/ui/components/providers/workflow/forms/index.ts
@@ -0,0 +1,6 @@
+export * from "./connect-account-form";
+export * from "./launch-scan-form";
+export * from "./radio-group-aws-via-credentials-form";
+export * from "./test-connection-form";
+export * from "./via-credentials-form";
+export * from "./via-role-form";
diff --git a/ui/components/providers/workflow/forms/launch-scan-form.tsx b/ui/components/providers/workflow/forms/launch-scan-form.tsx
new file mode 100644
index 0000000000..2532fd2e90
--- /dev/null
+++ b/ui/components/providers/workflow/forms/launch-scan-form.tsx
@@ -0,0 +1,162 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { scanOnDemand } from "@/actions/scans/scans";
+import { AddIcon } from "@/components/icons";
+import { CustomButton } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import { ProviderProps } from "@/types"; // Asegรบrate de importar la interfaz correcta
+import { launchScanFormSchema } from "@/types/formSchemas";
+
+import { ProviderInfo } from "../../provider-info";
+
+type FormValues = z.infer>;
+
+interface LaunchScanFormProps {
+ searchParams: { type: string; id: string };
+ providerData: {
+ data: {
+ type: string;
+ id: string;
+ attributes: ProviderProps["attributes"];
+ };
+ };
+}
+
+export const LaunchScanForm = ({
+ searchParams,
+ providerData,
+}: LaunchScanFormProps) => {
+ const providerType = searchParams.type;
+ const providerId = searchParams.id;
+
+ const [apiErrorMessage, setApiErrorMessage] = useState(null);
+ const router = useRouter();
+
+ const formSchema = launchScanFormSchema();
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerId,
+ providerType,
+ scannerArgs: {
+ checksToExecute: [],
+ },
+ },
+ });
+
+ // const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormValues) => {
+ const formData = new FormData();
+ formData.append("providerId", values.providerId);
+
+ // Generate default scan name using provider type and current date
+ const date = new Date();
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
+ const day = date.getDate().toString().padStart(2, "0");
+ const year = date.getFullYear();
+ const defaultScanName = `${providerType}:${month}/${day}/${year}`;
+
+ formData.append("scanName", defaultScanName);
+
+ try {
+ const data = await scanOnDemand(formData);
+
+ if (data.error) {
+ setApiErrorMessage(data.error);
+ form.setError("providerId", {
+ type: "server",
+ message: data.error,
+ });
+ } else {
+ router.push("/scans");
+ }
+ } catch (error) {
+ form.setError("providerId", {
+ type: "server",
+ message: "An unexpected error occurred. Please try again.",
+ });
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx b/ui/components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx
new file mode 100644
index 0000000000..7a870c79ff
--- /dev/null
+++ b/ui/components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { RadioGroup } from "@nextui-org/react";
+import React from "react";
+import { Control, Controller } from "react-hook-form";
+
+import { CustomRadio } from "@/components/ui/custom";
+import { FormMessage } from "@/components/ui/form";
+
+import { FormValues } from "./connect-account-form";
+
+type RadioGroupAWSViaCredentialsFormProps = {
+ control: Control;
+ isInvalid: boolean;
+ errorMessage?: string;
+};
+
+export const RadioGroupAWSViaCredentialsForm = ({
+ control,
+ isInvalid,
+ errorMessage,
+}: RadioGroupAWSViaCredentialsFormProps) => {
+ return (
+ (
+ <>
+
+
+
Using IAM Role
+
+
+ Connect assuming IAM Role
+
+
+
+ Using Credentials
+
+
+
+ Connect via Credentials
+
+
+
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+ >
+ )}
+ />
+ );
+};
diff --git a/ui/components/providers/workflow/forms/test-connection-form.tsx b/ui/components/providers/workflow/forms/test-connection-form.tsx
new file mode 100644
index 0000000000..6431c1a814
--- /dev/null
+++ b/ui/components/providers/workflow/forms/test-connection-form.tsx
@@ -0,0 +1,286 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Icon } from "@iconify/react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import {
+ checkConnectionProvider,
+ deleteCredentials,
+} from "@/actions/providers";
+import { scanOnDemand } from "@/actions/scans";
+import { getTask } from "@/actions/task/tasks";
+import { CheckIcon, SaveIcon } from "@/components/icons";
+import { useToast } from "@/components/ui";
+import { CustomButton } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import { checkTaskStatus } from "@/lib/helper";
+import { ApiError, testConnectionFormSchema } from "@/types";
+
+import { ProviderInfo } from "../..";
+
+type FormValues = z.infer;
+
+export const TestConnectionForm = ({
+ searchParams,
+ providerData,
+}: {
+ searchParams: { type: string; id: string };
+ providerData: {
+ data: {
+ id: string;
+ type: string;
+ attributes: {
+ connection: {
+ connected: boolean | null;
+ last_checked_at: string | null;
+ };
+ provider: "aws" | "azure" | "gcp" | "kubernetes";
+ alias: string;
+ scanner_args: Record;
+ };
+ relationships: {
+ secret: {
+ data: {
+ type: string;
+ id: string;
+ } | null;
+ };
+ };
+ };
+ };
+}) => {
+ const { toast } = useToast();
+ const router = useRouter();
+ const providerType = searchParams.type;
+ const providerId = searchParams.id;
+ console.log({ providerData }, "providerData from test connection form");
+ const formSchema = testConnectionFormSchema;
+ const [apiErrorMessage, setApiErrorMessage] = useState(null);
+ const [connectionStatus, setConnectionStatus] = useState<{
+ connected: boolean;
+ error: string | null;
+ } | null>(null);
+ const [isResettingCredentials, setIsResettingCredentials] = useState(false);
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerId,
+ },
+ });
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormValues) => {
+ console.log({ values }, "values from test connection form");
+ const formData = new FormData();
+ formData.append("providerId", values.providerId);
+
+ const data = await checkConnectionProvider(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ data.errors.forEach((error: ApiError) => {
+ const errorMessage = error.detail;
+
+ switch (errorMessage) {
+ case "Not found.":
+ setApiErrorMessage(errorMessage);
+ break;
+ default:
+ toast({
+ variant: "destructive",
+ title: `Error ${error.status}`,
+ description: errorMessage,
+ });
+ }
+ });
+ } else {
+ const taskId = data.data.id;
+ setApiErrorMessage(null);
+
+ // Use the helper function to check the task status
+ const taskResult = await checkTaskStatus(taskId);
+
+ if (taskResult.completed) {
+ const task = await getTask(taskId);
+ const { connected, error } = task.data.attributes.result;
+
+ setConnectionStatus({
+ connected,
+ error: connected ? null : error || "Unknown error",
+ });
+
+ if (connected) {
+ try {
+ const data = await scanOnDemand(formData);
+
+ if (data.error) {
+ setApiErrorMessage(data.error);
+ form.setError("providerId", {
+ type: "server",
+ message: data.error,
+ });
+ } else {
+ router.push(
+ `/providers/launch-scan?type=${providerType}&id=${providerId}`,
+ );
+ }
+ } catch (error) {
+ form.setError("providerId", {
+ type: "server",
+ message: "An unexpected error occurred. Please try again.",
+ });
+ }
+ } else {
+ setConnectionStatus({
+ connected: false,
+ error: error || "Connection failed, please review credentials.",
+ });
+ }
+ } else {
+ setConnectionStatus({
+ connected: false,
+ error: taskResult.error || "Unknown error",
+ });
+ }
+ }
+ };
+
+ const onResetCredentials = async () => {
+ setIsResettingCredentials(true);
+
+ // Check if provider the provider has no credentials
+ const providerSecretId =
+ providerData?.data?.relationships?.secret?.data?.id;
+ const hasNoCredentials = !providerSecretId;
+
+ if (hasNoCredentials) {
+ // If no credentials, redirect to add credentials page
+ router.push(
+ `/providers/add-credentials?type=${providerType}&id=${providerId}`,
+ );
+ return;
+ }
+
+ // If provider has credentials, delete them first
+ try {
+ await deleteCredentials(providerSecretId);
+ // After successful deletion, redirect to add credentials page
+ router.push(
+ `/providers/add-credentials?type=${providerType}&id=${providerId}`,
+ );
+ } catch (error) {
+ console.error("Failed to delete credentials:", error);
+ } finally {
+ setIsResettingCredentials(false);
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/providers/workflow/forms/via-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials-form.tsx
new file mode 100644
index 0000000000..176ed40a00
--- /dev/null
+++ b/ui/components/providers/workflow/forms/via-credentials-form.tsx
@@ -0,0 +1,207 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { SaveIcon } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { Control, useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { addCredentialsProvider } from "@/actions/providers/providers";
+import { useToast } from "@/components/ui";
+import { CustomButton } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import {
+ addCredentialsFormSchema,
+ ApiError,
+ AWSCredentials,
+ AzureCredentials,
+ GCPCredentials,
+ KubernetesCredentials,
+} from "@/types";
+
+import { AWScredentialsForm } from "./via-credentials/aws-credentials-form";
+import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
+import { GCPcredentialsForm } from "./via-credentials/gcp-credentials-form";
+import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
+
+type CredentialsFormSchema = z.infer<
+ ReturnType
+>;
+
+// Add this type intersection to include all fields
+type FormType = CredentialsFormSchema &
+ AWSCredentials &
+ AzureCredentials &
+ GCPCredentials &
+ KubernetesCredentials;
+
+export const ViaCredentialsForm = ({
+ searchParams,
+}: {
+ searchParams: { type: string; id: string };
+}) => {
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const providerType = searchParams.type;
+ const providerId = searchParams.id;
+ const formSchema = addCredentialsFormSchema(providerType);
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerId,
+ providerType,
+ ...(providerType === "aws"
+ ? {
+ aws_access_key_id: "",
+ aws_secret_access_key: "",
+ aws_session_token: "",
+ }
+ : providerType === "azure"
+ ? {
+ client_id: "",
+ client_secret: "",
+ tenant_id: "",
+ }
+ : providerType === "gcp"
+ ? {
+ client_id: "",
+ client_secret: "",
+ refresh_token: "",
+ }
+ : providerType === "kubernetes"
+ ? {
+ kubeconfig_content: "",
+ }
+ : {}),
+ },
+ });
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormType) => {
+ console.log("via credentials form", values);
+ const formData = new FormData();
+
+ Object.entries(values).forEach(
+ ([key, value]) => value !== undefined && formData.append(key, value),
+ );
+
+ const data = await addCredentialsProvider(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ data.errors.forEach((error: ApiError) => {
+ const errorMessage = error.detail;
+ switch (error.source.pointer) {
+ case "/data/attributes/secret/aws_access_key_id":
+ form.setError("aws_access_key_id", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/aws_secret_access_key":
+ form.setError("aws_secret_access_key", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/aws_session_token":
+ form.setError("aws_session_token", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/client_id":
+ form.setError("client_id", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/client_secret":
+ form.setError("client_secret", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/tenant_id":
+ form.setError("tenant_id", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/kubeconfig_content":
+ form.setError("kubeconfig_content", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/name":
+ form.setError("secretName", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ default:
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ }
+ });
+ } else {
+ router.push(
+ `/providers/test-connection?type=${providerType}&id=${providerId}`,
+ );
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx
new file mode 100644
index 0000000000..a024d138ff
--- /dev/null
+++ b/ui/components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx
@@ -0,0 +1,56 @@
+import { Control } from "react-hook-form";
+
+import { CustomInput } from "@/components/ui/custom";
+import { AWSCredentials } from "@/types";
+
+export const AWScredentialsForm = ({
+ control,
+}: {
+ control: Control;
+}) => {
+ return (
+ <>
+
+
+ Connect via Credentials
+
+
+ Please provide the information for your AWS credentials.
+
+
+
+
+
+ >
+ );
+};
diff --git a/ui/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx
new file mode 100644
index 0000000000..b78642b8d2
--- /dev/null
+++ b/ui/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx
@@ -0,0 +1,56 @@
+import { Control } from "react-hook-form";
+
+import { CustomInput } from "@/components/ui/custom";
+import { AzureCredentials } from "@/types";
+
+export const AzureCredentialsForm = ({
+ control,
+}: {
+ control: Control;
+}) => {
+ return (
+ <>
+
+
+ Connect via Credentials
+
+
+ Please provide the information for your Azure credentials.
+
+
+
+
+
+ >
+ );
+};
diff --git a/ui/components/providers/workflow/forms/via-credentials/gcp-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/gcp-credentials-form.tsx
new file mode 100644
index 0000000000..784f173f49
--- /dev/null
+++ b/ui/components/providers/workflow/forms/via-credentials/gcp-credentials-form.tsx
@@ -0,0 +1,56 @@
+import { Control } from "react-hook-form";
+
+import { CustomInput } from "@/components/ui/custom";
+import { GCPCredentials } from "@/types";
+
+export const GCPcredentialsForm = ({
+ control,
+}: {
+ control: Control;
+}) => {
+ return (
+ <>
+
+
+ Connect via Credentials
+
+
+ Please provide the information for your GCP credentials.
+
+
+
+
+
+ >
+ );
+};
diff --git a/ui/components/providers/workflow/forms/via-credentials/index.ts b/ui/components/providers/workflow/forms/via-credentials/index.ts
new file mode 100644
index 0000000000..d3198a1d0e
--- /dev/null
+++ b/ui/components/providers/workflow/forms/via-credentials/index.ts
@@ -0,0 +1,4 @@
+export * from "./aws-credentials-form";
+export * from "./azure-credentials-form";
+export * from "./gcp-credentials-form";
+export * from "./k8s-credentials-form";
diff --git a/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx
new file mode 100644
index 0000000000..0725d134eb
--- /dev/null
+++ b/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx
@@ -0,0 +1,34 @@
+import { Control } from "react-hook-form";
+
+import { CustomInput } from "@/components/ui/custom";
+import { KubernetesCredentials } from "@/types";
+
+export const KubernetesCredentialsForm = ({
+ control,
+}: {
+ control: Control;
+}) => {
+ return (
+ <>
+
+
+ Connect via Credentials
+
+
+ Please provide the information for your Kubernetes credentials.
+
+
+
+ >
+ );
+};
diff --git a/ui/components/providers/workflow/forms/via-role-form.tsx b/ui/components/providers/workflow/forms/via-role-form.tsx
new file mode 100644
index 0000000000..39ba301447
--- /dev/null
+++ b/ui/components/providers/workflow/forms/via-role-form.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { SaveIcon } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { Control, useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { addCredentialsProvider } from "@/actions/providers/providers";
+import { useToast } from "@/components/ui";
+import { CustomButton } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import {
+ addCredentialsRoleFormSchema,
+ ApiError,
+ AWSCredentialsRole,
+} from "@/types";
+
+import { AWSCredentialsRoleForm } from "./via-role/aws-role-form";
+
+export const ViaRoleForm = ({
+ searchParams,
+}: {
+ searchParams: { type: string; id: string };
+}) => {
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const providerType = searchParams.type;
+ const providerId = searchParams.id;
+
+ const formSchema = addCredentialsRoleFormSchema(providerType);
+ type FormSchemaType = z.infer;
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerId,
+ providerType,
+ ...(providerType === "aws"
+ ? {
+ role_arn: "",
+ aws_access_key_id: "",
+ aws_secret_access_key: "",
+ aws_session_token: "",
+ session_duration: 3600,
+ external_id: "",
+ role_session_name: "",
+ }
+ : {}),
+ },
+ });
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormSchemaType) => {
+ console.log("via ROLE form", values);
+ const formData = new FormData();
+
+ Object.entries(values).forEach(
+ ([key, value]) =>
+ value !== undefined && formData.append(key, String(value)),
+ );
+
+ const data = await addCredentialsProvider(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ data.errors.forEach((error: ApiError) => {
+ const errorMessage = error.detail;
+ switch (error.source.pointer) {
+ case "/data/attributes/secret/role_arn":
+ form.setError("role_arn" as keyof FormSchemaType, {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+
+ default:
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ }
+ });
+ } else {
+ router.push(
+ `/providers/test-connection?type=${providerType}&id=${providerId}`,
+ );
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/providers/workflow/forms/via-role/aws-role-form.tsx b/ui/components/providers/workflow/forms/via-role/aws-role-form.tsx
new file mode 100644
index 0000000000..eb7f5d3a43
--- /dev/null
+++ b/ui/components/providers/workflow/forms/via-role/aws-role-form.tsx
@@ -0,0 +1,105 @@
+import { Control } from "react-hook-form";
+
+import { CustomInput } from "@/components/ui/custom";
+import { AWSCredentialsRole } from "@/types";
+
+export const AWSCredentialsRoleForm = ({
+ control,
+}: {
+ control: Control;
+}) => {
+ return (
+ <>
+
+
+ Connect assuming IAM Role
+
+
+ Please provide the information for your AWS credentials.
+
+
+
+
+ Optional fields
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/ui/components/providers/workflow/forms/via-role/index.ts b/ui/components/providers/workflow/forms/via-role/index.ts
new file mode 100644
index 0000000000..763c90c0c0
--- /dev/null
+++ b/ui/components/providers/workflow/forms/via-role/index.ts
@@ -0,0 +1 @@
+export * from "./aws-role-form";
diff --git a/ui/components/providers/workflow/index.ts b/ui/components/providers/workflow/index.ts
new file mode 100644
index 0000000000..fca9e92617
--- /dev/null
+++ b/ui/components/providers/workflow/index.ts
@@ -0,0 +1,2 @@
+export * from "./vertical-steps";
+export * from "./workflow-add-provider";
diff --git a/ui/components/providers/workflow/vertical-steps.tsx b/ui/components/providers/workflow/vertical-steps.tsx
new file mode 100644
index 0000000000..74eaa32747
--- /dev/null
+++ b/ui/components/providers/workflow/vertical-steps.tsx
@@ -0,0 +1,291 @@
+"use client";
+
+import type { ButtonProps } from "@nextui-org/react";
+import { cn } from "@nextui-org/react";
+import { useControlledState } from "@react-stately/utils";
+import { domAnimation, LazyMotion, m } from "framer-motion";
+import type { ComponentProps } from "react";
+import React from "react";
+
+export type VerticalStepProps = {
+ className?: string;
+ description?: React.ReactNode;
+ title?: React.ReactNode;
+};
+
+export interface VerticalStepsProps
+ extends React.HTMLAttributes {
+ /**
+ * An array of steps.
+ *
+ * @default []
+ */
+ steps?: VerticalStepProps[];
+ /**
+ * The color of the steps.
+ *
+ * @default "primary"
+ */
+ color?: ButtonProps["color"];
+ /**
+ * The current step index.
+ */
+ currentStep?: number;
+ /**
+ * The default step index.
+ *
+ * @default 0
+ */
+ defaultStep?: number;
+ /**
+ * Whether to hide the progress bars.
+ *
+ * @default false
+ */
+ hideProgressBars?: boolean;
+ /**
+ * The custom class for the steps wrapper.
+ */
+ className?: string;
+ /**
+ * The custom class for the step.
+ */
+ stepClassName?: string;
+ /**
+ * Callback function when the step index changes.
+ */
+ onStepChange?: (stepIndex: number) => void;
+}
+
+function CheckIcon(props: ComponentProps<"svg">) {
+ return (
+
+ );
+}
+
+export const VerticalSteps = React.forwardRef<
+ HTMLButtonElement,
+ VerticalStepsProps
+>(
+ (
+ {
+ color = "primary",
+ steps = [],
+ defaultStep = 0,
+ onStepChange,
+ currentStep: currentStepProp,
+ hideProgressBars = false,
+ stepClassName,
+ className,
+ ...props
+ },
+ ref,
+ ) => {
+ const [currentStep, setCurrentStep] = useControlledState(
+ currentStepProp,
+ defaultStep,
+ onStepChange,
+ );
+
+ const colors = React.useMemo(() => {
+ let userColor;
+ let fgColor;
+
+ const colorsVars = [
+ "[--active-fg-color:var(--step-fg-color)]",
+ "[--active-border-color:var(--step-color)]",
+ "[--active-color:var(--step-color)]",
+ "[--complete-background-color:var(--step-color)]",
+ "[--complete-border-color:var(--step-color)]",
+ "[--inactive-border-color:hsl(var(--nextui-default-300))]",
+ "[--inactive-color:hsl(var(--nextui-default-300))]",
+ ];
+
+ switch (color) {
+ case "primary":
+ userColor = "[--step-color:hsl(var(--nextui-primary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-primary-foreground))]";
+ break;
+ case "secondary":
+ userColor = "[--step-color:hsl(var(--nextui-secondary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-secondary-foreground))]";
+ break;
+ case "success":
+ userColor = "[--step-color:hsl(var(--nextui-success))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-success-foreground))]";
+ break;
+ case "warning":
+ userColor = "[--step-color:hsl(var(--nextui-warning))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-warning-foreground))]";
+ break;
+ case "danger":
+ userColor = "[--step-color:hsl(var(--nextui-error))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-error-foreground))]";
+ break;
+ case "default":
+ userColor = "[--step-color:hsl(var(--nextui-default))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-default-foreground))]";
+ break;
+ default:
+ userColor = "[--step-color:hsl(var(--nextui-primary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-primary-foreground))]";
+ break;
+ }
+
+ if (!className?.includes("--step-fg-color")) colorsVars.unshift(fgColor);
+ if (!className?.includes("--step-color")) colorsVars.unshift(userColor);
+ if (!className?.includes("--inactive-bar-color"))
+ colorsVars.push(
+ "[--inactive-bar-color:hsl(var(--nextui-default-300))]",
+ );
+
+ return colorsVars;
+ }, [color, className]);
+
+ return (
+
+ );
+ },
+);
+
+VerticalSteps.displayName = "VerticalSteps";
diff --git a/ui/components/providers/workflow/workflow-add-provider.tsx b/ui/components/providers/workflow/workflow-add-provider.tsx
new file mode 100644
index 0000000000..1f2e631f06
--- /dev/null
+++ b/ui/components/providers/workflow/workflow-add-provider.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { Progress, Spacer } from "@nextui-org/react";
+import { usePathname } from "next/navigation";
+import React from "react";
+
+import { VerticalSteps } from "./vertical-steps";
+
+const steps = [
+ {
+ title: "Add your cloud account",
+ description:
+ "Select the cloud provider of the account you want to connect and choose whether to use IAM role or credentials for access.",
+ href: "/providers/connect-account",
+ },
+ {
+ title: "Add credentials to your cloud account",
+ description: "Add the credentials needed to connect to your cloud account.",
+ href: "/providers/add-credentials",
+ },
+ {
+ title: "Test connection",
+ description:
+ "Test your connection to verify that the credentials provided are valid for accessing your cloud account.",
+ href: "/providers/test-connection",
+ },
+ {
+ title: "Launch scan",
+ description:
+ "Launch the scan now or schedule it for a later date and time.",
+ href: "/providers/launch-scan",
+ },
+];
+
+export const WorkflowAddProvider = () => {
+ const pathname = usePathname();
+
+ // Calculate current step based on pathname
+ const currentStepIndex = steps.findIndex((step) =>
+ pathname.endsWith(step.href),
+ );
+ const currentStep = currentStepIndex === -1 ? 0 : currentStepIndex;
+
+ return (
+
+
+ Add a cloud account
+
+
+ Follow the steps to configure your cloud account. This allows you to
+ launch the first scan when the process is complete.
+
+
+
+
+
+ );
+};
diff --git a/ui/components/scans/button-refresh-data.tsx b/ui/components/scans/button-refresh-data.tsx
new file mode 100644
index 0000000000..41d05e3981
--- /dev/null
+++ b/ui/components/scans/button-refresh-data.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { RefreshCcwIcon } from "lucide-react";
+import { useTransition } from "react";
+
+import { CustomButton } from "../ui/custom";
+
+export const ButtonRefreshData = ({
+ onPress,
+}: {
+ onPress: () => Promise;
+}) => {
+ const [isPending, startTransition] = useTransition();
+
+ return (
+ }
+ isLoading={isPending}
+ onPress={() => {
+ startTransition(async () => {
+ await onPress();
+ });
+ }}
+ />
+ );
+};
diff --git a/ui/components/scans/forms/edit-scan-form.tsx b/ui/components/scans/forms/edit-scan-form.tsx
new file mode 100644
index 0000000000..62c6e338b6
--- /dev/null
+++ b/ui/components/scans/forms/edit-scan-form.tsx
@@ -0,0 +1,119 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Dispatch, SetStateAction } from "react";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { updateScan } from "@/actions/scans";
+import { SaveIcon } from "@/components/icons";
+import { useToast } from "@/components/ui";
+import { CustomButton, CustomInput } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import { editScanFormSchema } from "@/types";
+
+export const EditScanForm = ({
+ scanId,
+ scanName,
+ setIsOpen,
+}: {
+ scanId: string;
+ scanName: string;
+ setIsOpen: Dispatch>;
+}) => {
+ const formSchema = editScanFormSchema(scanName);
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ scanId: scanId,
+ scanName: scanName,
+ },
+ });
+
+ const { toast } = useToast();
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: z.infer) => {
+ const formData = new FormData();
+
+ Object.entries(values).forEach(
+ ([key, value]) => value !== undefined && formData.append(key, value),
+ );
+
+ const data = await updateScan(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ const error = data.errors[0];
+ const errorMessage = `${error.detail}`;
+ // show error
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ } else {
+ toast({
+ title: "Success!",
+ description: "The scan was updated successfully.",
+ });
+ setIsOpen(false); // Close the modal on success
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/scans/forms/index.ts b/ui/components/scans/forms/index.ts
new file mode 100644
index 0000000000..4acd302cb9
--- /dev/null
+++ b/ui/components/scans/forms/index.ts
@@ -0,0 +1,2 @@
+export * from "./edit-scan-form";
+export * from "./schedule-form";
diff --git a/ui/components/scans/forms/schedule-form.tsx b/ui/components/scans/forms/schedule-form.tsx
new file mode 100644
index 0000000000..2b75a3c263
--- /dev/null
+++ b/ui/components/scans/forms/schedule-form.tsx
@@ -0,0 +1,113 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Dispatch, SetStateAction } from "react";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { updateProvider } from "@/actions/providers";
+import { SaveIcon } from "@/components/icons";
+import { useToast } from "@/components/ui";
+import { CustomButton, CustomInput } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import { scheduleScanFormSchema } from "@/types";
+
+export const ScheduleForm = ({
+ providerId,
+ scheduleDate,
+ setIsOpen,
+}: {
+ providerId: string;
+ scheduleDate: string;
+ setIsOpen: Dispatch>;
+}) => {
+ const formSchema = scheduleScanFormSchema();
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerId: providerId,
+ scheduleDate: scheduleDate,
+ },
+ });
+
+ const { toast } = useToast();
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: z.infer) => {
+ const formData = new FormData();
+
+ Object.entries(values).forEach(
+ ([key, value]) => value !== undefined && formData.append(key, value),
+ );
+ const data = await updateProvider(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ const error = data.errors[0];
+ const errorMessage = `${error.detail}`;
+ // show error
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ } else {
+ toast({
+ title: "Success!",
+ description: "The scan was scheduled successfully.",
+ });
+ setIsOpen(false); // Close the modal on success
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/scans/index.ts b/ui/components/scans/index.ts
new file mode 100644
index 0000000000..7d435fad43
--- /dev/null
+++ b/ui/components/scans/index.ts
@@ -0,0 +1 @@
+export * from "./button-refresh-data";
diff --git a/ui/components/scans/launch-workflow/index.ts b/ui/components/scans/launch-workflow/index.ts
new file mode 100644
index 0000000000..4f8a794d33
--- /dev/null
+++ b/ui/components/scans/launch-workflow/index.ts
@@ -0,0 +1,2 @@
+export * from "./launch-scan-workflow-form";
+export * from "./select-scan-provider";
diff --git a/ui/components/scans/launch-workflow/launch-scan-workflow-form.tsx b/ui/components/scans/launch-workflow/launch-scan-workflow-form.tsx
new file mode 100644
index 0000000000..a19acfea42
--- /dev/null
+++ b/ui/components/scans/launch-workflow/launch-scan-workflow-form.tsx
@@ -0,0 +1,194 @@
+"use client";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { AnimatePresence, motion } from "framer-motion";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { scanOnDemand } from "@/actions/scans";
+import { RocketIcon, ScheduleIcon } from "@/components/icons";
+import { CustomButton, CustomInput } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import { toast } from "@/components/ui/toast";
+import { onDemandScanFormSchema } from "@/types";
+
+import { SelectScanProvider } from "./select-scan-provider";
+
+type ProviderInfo = {
+ providerId: string;
+ alias: string;
+ providerType: string;
+ uid: string;
+ connected: boolean;
+};
+
+export const LaunchScanWorkflow = ({
+ providers,
+}: {
+ providers: ProviderInfo[];
+}) => {
+ const formSchema = onDemandScanFormSchema();
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerId: "",
+ scanName: "",
+ scannerArgs: undefined,
+ },
+ });
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: z.infer) => {
+ const formValues = { ...values };
+
+ const formData = new FormData();
+
+ // Loop through form values and add to formData
+ Object.entries(formValues).forEach(
+ ([key, value]) =>
+ value !== undefined &&
+ formData.append(
+ key,
+ typeof value === "object" ? JSON.stringify(value) : value,
+ ),
+ );
+
+ const data = await scanOnDemand(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ const error = data.errors[0];
+ const errorMessage = `${error.detail}`;
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ } else {
+ toast({
+ title: "Success!",
+ description: "The scan was launched successfully.",
+ });
+ // Reset form after successful submission
+ form.reset();
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/scans/launch-workflow/select-scan-provider.tsx b/ui/components/scans/launch-workflow/select-scan-provider.tsx
new file mode 100644
index 0000000000..c1805f488b
--- /dev/null
+++ b/ui/components/scans/launch-workflow/select-scan-provider.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { Select, SelectItem } from "@nextui-org/react";
+import { Control, FieldPath, FieldValues } from "react-hook-form";
+
+import { AWSProviderBadge } from "@/components/icons/providers-badge/AWSProviderBadge";
+import { AzureProviderBadge } from "@/components/icons/providers-badge/AzureProviderBadge";
+import { GCPProviderBadge } from "@/components/icons/providers-badge/GCPProviderBadge";
+import { KS8ProviderBadge } from "@/components/icons/providers-badge/KS8ProviderBadge";
+import { FormControl, FormField, FormMessage } from "@/components/ui/form";
+
+interface SelectScanProviderProps<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+> {
+ providers: {
+ providerId: string;
+ alias: string;
+ providerType: string;
+ uid: string;
+ connected: boolean;
+ }[];
+ control: Control;
+ name: TName;
+}
+
+export const SelectScanProvider = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+>({
+ providers,
+ control,
+ name,
+}: SelectScanProviderProps) => {
+ const renderBadge = (providerType: string) => {
+ switch (providerType) {
+ case "aws":
+ return ;
+ case "azure":
+ return ;
+ case "gcp":
+ return ;
+ case "kubernetes":
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ return (
+ (
+ <>
+
+
+
+
+ >
+ )}
+ />
+ );
+};
diff --git a/ui/components/scans/table/index.ts b/ui/components/scans/table/index.ts
new file mode 100644
index 0000000000..0a8aee6de4
--- /dev/null
+++ b/ui/components/scans/table/index.ts
@@ -0,0 +1,2 @@
+export * from "./scan-detail";
+export * from "./skeleton-table-scans";
diff --git a/ui/components/scans/table/scan-detail.tsx b/ui/components/scans/table/scan-detail.tsx
new file mode 100644
index 0000000000..d6d9fedf7b
--- /dev/null
+++ b/ui/components/scans/table/scan-detail.tsx
@@ -0,0 +1,214 @@
+"use client";
+
+import { Card, CardBody, CardHeader, Divider } from "@nextui-org/react";
+
+import { DateWithTime, SnippetId } from "@/components/ui/entities";
+import { StatusBadge } from "@/components/ui/table/status-badge";
+import { ScanProps, TaskDetails } from "@/types";
+
+interface ScanDetailsProps {
+ scanDetails: ScanProps & {
+ taskDetails?: TaskDetails;
+ };
+}
+
+export const ScanDetail = ({ scanDetails }: ScanDetailsProps) => {
+ const scanOnDemand = scanDetails.attributes;
+ const taskDetails = scanDetails.taskDetails;
+
+ return (
+
+ {/* Header */}
+
+
+ Scan Details
+
+
+
+
+
+
+ {/* Details Section */}
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+ ) : (
+ "Not Started"
+ )
+ }
+ />
+
+ ) : (
+ "Not Completed"
+ )
+ }
+ />
+
+ ) : (
+ "Not Scheduled"
+ )
+ }
+ />
+
+ }
+ />
+
+ ) : (
+ "N/A"
+ )
+ }
+ />
+
+
+
+
+ {/* Scan Arguments Section */}
+
+
+
+ Scan Arguments
+
+
+
+
+
+
+ Checks
+
+
+ {(scanOnDemand.scanner_args as any)?.checks_to_execute?.join(
+ ", ",
+ ) || "N/A"}
+
+
+
+
+
+ {/* Task Details Section */}
+ {taskDetails && (
+
+
+
+ State Details
+
+
+
+
+
+
+
+ {taskDetails.attributes.result && (
+ <>
+
+ {taskDetails.attributes.result.exc_message && (
+
+ )}
+ >
+ )}
+
+
+
+
+ )}
+
+ );
+};
+
+const DateItem = ({
+ label,
+ value,
+}: {
+ label: string;
+ value: React.ReactNode;
+}) => (
+
+
+ {label}:
+
+
{value}
+
+);
+
+const DetailItem = ({
+ label,
+ value,
+}: {
+ label: string;
+ value: React.ReactNode;
+}) => (
+
+
+ {label}:
+
+
{value}
+
+);
diff --git a/ui/components/scans/table/scans/column-get-scans.tsx b/ui/components/scans/table/scans/column-get-scans.tsx
new file mode 100644
index 0000000000..ed235d2965
--- /dev/null
+++ b/ui/components/scans/table/scans/column-get-scans.tsx
@@ -0,0 +1,165 @@
+"use client";
+
+import { ColumnDef } from "@tanstack/react-table";
+import { useSearchParams } from "next/navigation";
+
+import { InfoIcon } from "@/components/icons";
+import { DateWithTime, SnippetId } from "@/components/ui/entities";
+import { TriggerSheet } from "@/components/ui/sheet";
+import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table";
+import { ScanProps } from "@/types";
+
+import { DataTableRowActions } from "./data-table-row-actions";
+import { DataTableRowDetails } from "./data-table-row-details";
+
+const getScanData = (row: { original: ScanProps }) => {
+ return row.original;
+};
+
+export const ColumnGetScans: ColumnDef[] = [
+ {
+ accessorKey: "started_at",
+ header: () => Started at
,
+ cell: ({ row }) => {
+ const {
+ attributes: { started_at },
+ } = getScanData(row);
+
+ return ;
+ },
+ },
+ {
+ accessorKey: "status",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { state },
+ } = getScanData(row);
+ return (
+
+ );
+ },
+ },
+ // {
+ // accessorKey: "scanner_args",
+ // header: "Scanner Args",
+ // cell: ({ row }) => {
+ // const {
+ // attributes: { scanner_args },
+ // } = getScanData(row);
+ // return {scanner_args?.only_logs}
;
+ // },
+ // },
+ {
+ accessorKey: "resources",
+ header: "Resources",
+ cell: ({ row }) => {
+ const {
+ attributes: { unique_resource_count },
+ } = getScanData(row);
+ return {unique_resource_count}
;
+ },
+ },
+ {
+ accessorKey: "scheduled_at",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { scheduled_at },
+ } = getScanData(row);
+ return ;
+ },
+ },
+
+ {
+ accessorKey: "completed_at",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { completed_at },
+ } = getScanData(row);
+ return ;
+ },
+ },
+ {
+ accessorKey: "trigger",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { trigger },
+ } = getScanData(row);
+ return {trigger}
;
+ },
+ },
+
+ {
+ accessorKey: "id",
+ header: () => ID,
+ cell: ({ row }) => {
+ return ;
+ },
+ },
+ {
+ accessorKey: "name",
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => {
+ const {
+ attributes: { name },
+ } = getScanData(row);
+
+ if (!name || name.length === 0) {
+ return -;
+ }
+
+ return {name};
+ },
+ },
+ {
+ id: "moreInfo",
+ header: "Details",
+ cell: ({ row }) => {
+ const searchParams = useSearchParams();
+ const scanId = searchParams.get("scanId");
+ const isOpen = scanId === row.original.id;
+
+ return (
+ }
+ title="Scan Details"
+ description="View the scan details"
+ defaultOpen={isOpen}
+ >
+
+
+ );
+ },
+ },
+
+ {
+ id: "actions",
+ cell: ({ row }) => {
+ return ;
+ },
+ },
+];
diff --git a/ui/components/scans/table/scans/data-table-row-actions.tsx b/ui/components/scans/table/scans/data-table-row-actions.tsx
new file mode 100644
index 0000000000..3909fc4533
--- /dev/null
+++ b/ui/components/scans/table/scans/data-table-row-actions.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import {
+ Button,
+ Dropdown,
+ DropdownItem,
+ DropdownMenu,
+ DropdownSection,
+ DropdownTrigger,
+} from "@nextui-org/react";
+import {
+ // DeleteDocumentBulkIcon,
+ EditDocumentBulkIcon,
+} from "@nextui-org/shared-icons";
+import { Row } from "@tanstack/react-table";
+// import clsx from "clsx";
+import { useState } from "react";
+
+import { VerticalDotsIcon } from "@/components/icons";
+import { CustomAlertModal } from "@/components/ui/custom";
+
+import { EditScanForm } from "../../forms";
+
+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 scanId = (row.original as { id: string }).id;
+ const scanName = (row.original as any).attributes?.name;
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ }
+ onClick={() => setIsEditOpen(true)}
+ >
+ Edit Scan
+
+
+
+
+
+ >
+ );
+}
diff --git a/ui/components/scans/table/scans/data-table-row-details.tsx b/ui/components/scans/table/scans/data-table-row-details.tsx
new file mode 100644
index 0000000000..92adbbe18c
--- /dev/null
+++ b/ui/components/scans/table/scans/data-table-row-details.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import { useRouter, useSearchParams } from "next/navigation";
+import { useEffect, useState } from "react";
+
+import { getScan } from "@/actions/scans";
+import { getTask } from "@/actions/task";
+import { ScanDetail, SkeletonTableScans } from "@/components/scans/table";
+import { checkTaskStatus } from "@/lib";
+import { ScanProps } from "@/types";
+
+export const DataTableRowDetails = ({ entityId }: { entityId: string }) => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const [scanDetails, setScanDetails] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ // Add scanId to URL
+ const params = new URLSearchParams(searchParams.toString());
+ params.set("scanId", entityId);
+ router.push(`?${params.toString()}`, { scroll: false });
+
+ // Cleanup function: remove scanId from URL when component unmounts
+ return () => {
+ const newParams = new URLSearchParams(searchParams.toString());
+ newParams.delete("scanId");
+ router.push(`?${newParams.toString()}`, { scroll: false });
+ };
+ }, [entityId, router, searchParams]);
+
+ useEffect(() => {
+ const fetchScanDetails = async () => {
+ try {
+ const result = await getScan(entityId);
+
+ const taskId = result.data.relationships.task?.data?.id;
+
+ if (taskId) {
+ const taskResult = await checkTaskStatus(taskId);
+
+ if (taskResult.completed !== undefined) {
+ const task = await getTask(taskId);
+ setScanDetails({
+ ...result.data,
+ taskDetails: task.data,
+ });
+ }
+ } else {
+ setScanDetails(result.data);
+ }
+ } catch (error) {
+ console.error("Error in fetchScanDetails:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchScanDetails();
+ }, [entityId]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!scanDetails) {
+ return No scan details available
;
+ }
+
+ return ;
+};
diff --git a/ui/components/scans/table/scans/index.ts b/ui/components/scans/table/scans/index.ts
new file mode 100644
index 0000000000..2567c0098c
--- /dev/null
+++ b/ui/components/scans/table/scans/index.ts
@@ -0,0 +1,3 @@
+export * from "./column-get-scans";
+export * from "./data-table-row-actions";
+export * from "./data-table-row-details";
diff --git a/ui/components/scans/table/skeleton-table-scans.tsx b/ui/components/scans/table/skeleton-table-scans.tsx
new file mode 100644
index 0000000000..a0c4da0cf4
--- /dev/null
+++ b/ui/components/scans/table/skeleton-table-scans.tsx
@@ -0,0 +1,65 @@
+import { Card, Skeleton } from "@nextui-org/react";
+import React from "react";
+
+export const SkeletonTableScans = () => {
+ return (
+
+ {/* Table headers */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Table body */}
+
+ {[...Array(3)].map((_, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/ui/components/services/ServiceCard.tsx b/ui/components/services/ServiceCard.tsx
new file mode 100644
index 0000000000..07ac6c3591
--- /dev/null
+++ b/ui/components/services/ServiceCard.tsx
@@ -0,0 +1,47 @@
+import { Card, CardBody, Chip } from "@nextui-org/react";
+
+import { getAWSIcon, NotificationIcon, SuccessIcon } from "../icons";
+
+interface CardServiceProps {
+ fidingsFailed: number;
+ serviceAlias: string;
+}
+export const ServiceCard: React.FC = ({
+ fidingsFailed,
+ serviceAlias,
+}) => {
+ return (
+
+
+
+ {getAWSIcon(serviceAlias)}
+
+
{serviceAlias}
+
+ {fidingsFailed > 0
+ ? `${fidingsFailed} Failed Findings`
+ : "No failed findings"}
+
+
+
+
+ 0 ? (
+
+ ) : (
+
+ )
+ }
+ color={fidingsFailed > 0 ? "danger" : "success"}
+ radius="full"
+ size="md"
+ >
+ {fidingsFailed > 0 ? fidingsFailed : ""}
+
+
+
+ );
+};
diff --git a/ui/components/services/ServiceSkeletonGrid.tsx b/ui/components/services/ServiceSkeletonGrid.tsx
new file mode 100644
index 0000000000..2e43ced7a0
--- /dev/null
+++ b/ui/components/services/ServiceSkeletonGrid.tsx
@@ -0,0 +1,18 @@
+import { Card, Skeleton } from "@nextui-org/react";
+import React from "react";
+
+export const ServiceSkeletonGrid = () => {
+ return (
+
+
+ {[...Array(25)].map((_, index) => (
+
+ ))}
+
+
+ );
+};
diff --git a/ui/components/services/index.ts b/ui/components/services/index.ts
new file mode 100644
index 0000000000..d75bdbfb98
--- /dev/null
+++ b/ui/components/services/index.ts
@@ -0,0 +1,2 @@
+export * from "./ServiceCard";
+export * from "./ServiceSkeletonGrid";
diff --git a/ui/components/ui/action-card/ActionCard.tsx b/ui/components/ui/action-card/ActionCard.tsx
new file mode 100644
index 0000000000..ea7e8295ce
--- /dev/null
+++ b/ui/components/ui/action-card/ActionCard.tsx
@@ -0,0 +1,84 @@
+"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;
+};
+
+export 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";
diff --git a/ui/components/ui/alert-dialog/AlertDialog.tsx b/ui/components/ui/alert-dialog/AlertDialog.tsx
new file mode 100644
index 0000000000..26b8aec946
--- /dev/null
+++ b/ui/components/ui/alert-dialog/AlertDialog.tsx
@@ -0,0 +1,131 @@
+"use client";
+
+import { cn } from "@nextui-org/react";
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+import * as React from "react";
+
+const AlertDialog = AlertDialogPrimitive.Root;
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+));
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogHeader.displayName = "AlertDialogHeader";
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogFooter.displayName = "AlertDialogFooter";
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName;
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
+
+export {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ AlertDialogPortal,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+};
diff --git a/ui/components/ui/alert/Alert.tsx b/ui/components/ui/alert/Alert.tsx
new file mode 100644
index 0000000000..49afe61c80
--- /dev/null
+++ b/ui/components/ui/alert/Alert.tsx
@@ -0,0 +1,61 @@
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border border-slate-200 px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-slate-950 [&>svg~*]:pl-7 dark:border-slate-800 dark:[&>svg]:text-slate-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50",
+ destructive:
+ "border-red-500/50 text-red-500 dark:border-red-500 [&>svg]:text-red-500 dark:border-red-900/50 dark:text-red-900 dark:dark:border-red-900 dark:[&>svg]:text-red-900",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+));
+Alert.displayName = "Alert";
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+));
+AlertTitle.displayName = "AlertTitle";
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = "AlertDescription";
+
+export { Alert, AlertDescription, AlertTitle };
diff --git a/ui/components/ui/chart/Chart.tsx b/ui/components/ui/chart/Chart.tsx
new file mode 100644
index 0000000000..9d096726f5
--- /dev/null
+++ b/ui/components/ui/chart/Chart.tsx
@@ -0,0 +1,371 @@
+"use client";
+
+import * as React from "react";
+import * as RechartsPrimitive from "recharts";
+
+// import {
+// NameType,
+// Payload,
+// ValueType,
+// } from "recharts/types/component/DefaultTooltipContent";
+import { cn } from "@/lib/utils";
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const;
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode;
+ icon?: React.ComponentType;
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ );
+};
+
+type ChartContextProps = {
+ config: ChartConfig;
+};
+
+const ChartContext = React.createContext(null);
+
+function useChart() {
+ const context = React.useContext(ChartContext);
+
+ if (!context) {
+ throw new Error("useChart must be used within a ");
+ }
+
+ return context;
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig;
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"];
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId();
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+});
+ChartContainer.displayName = "Chart";
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color,
+ );
+
+ if (!colorConfig.length) {
+ return null;
+ }
+
+ return (
+