mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
chore(ui): Merge UI repository
This commit is contained in:
15
ui/.dockerignore
Normal file
15
ui/.dockerignore
Normal file
@@ -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
|
||||
6
ui/.env.template
Normal file
6
ui/.env.template
Normal file
@@ -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
|
||||
20
ui/.eslintignore
Normal file
20
ui/.eslintignore
Normal file
@@ -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
|
||||
44
ui/.eslintrc.cjs
Normal file
44
ui/.eslintrc.cjs
Normal file
@@ -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: "^_" }],
|
||||
},
|
||||
};
|
||||
5
ui/.github/CODEOWNERS
vendored
Normal file
5
ui/.github/CODEOWNERS
vendored
Normal file
@@ -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
|
||||
7
ui/.github/pull_request_template.md
vendored
Normal file
7
ui/.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
### Description
|
||||
|
||||
What was done in this PR
|
||||
|
||||
### How to Review
|
||||
|
||||
The reviewer should verify all these steps:
|
||||
29
ui/.github/workflows/checks.yml
vendored
Normal file
29
ui/.github/workflows/checks.yml
vendored
Normal file
@@ -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
|
||||
36
ui/.gitignore
vendored
Normal file
36
ui/.gitignore
vendored
Normal file
@@ -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
|
||||
1
ui/.husky/pre-commit
Normal file
1
ui/.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npm run healthcheck
|
||||
1
ui/.prettierignore
Normal file
1
ui/.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
10
ui/.prettierrc.json
Normal file
10
ui/.prettierrc.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"bracketSpacing": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"printWidth": 80,
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
11
ui/.vscode/settings.json
vendored
Normal file
11
ui/.vscode/settings.json
vendored
Normal file
@@ -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
|
||||
}
|
||||
70
ui/Dockerfile
Normal file
70
ui/Dockerfile
Normal file
@@ -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"]
|
||||
114
ui/README.md
Normal file
114
ui/README.md
Normal file
@@ -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)
|
||||
169
ui/actions/auth/auth.ts
Normal file
169
ui/actions/auth/auth.ts
Normal file
@@ -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<typeof formSchemaSignIn> = {
|
||||
email: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
export async function authenticate(
|
||||
prevState: unknown,
|
||||
formData: z.infer<typeof formSchemaSignIn>,
|
||||
) {
|
||||
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<typeof formSchemaSignUp>,
|
||||
) => {
|
||||
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<typeof formSchemaSignIn>) => {
|
||||
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();
|
||||
}
|
||||
1
ui/actions/auth/index.ts
Normal file
1
ui/actions/auth/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./auth";
|
||||
43
ui/actions/compliances/compliances.ts
Normal file
43
ui/actions/compliances/compliances.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
"use server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
import { auth } from "@/auth.config";
|
||||
import { parseStringify } from "@/lib";
|
||||
|
||||
export const getCompliancesOverview = async ({
|
||||
scanId,
|
||||
region,
|
||||
}: {
|
||||
scanId: string;
|
||||
region?: string | string[];
|
||||
}) => {
|
||||
const session = await auth();
|
||||
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
const url = new URL(`${keyServer}/compliance-overviews`);
|
||||
|
||||
if (scanId) url.searchParams.append("filter[scan_id]", scanId);
|
||||
|
||||
if (region) {
|
||||
const regionValue = Array.isArray(region) ? region.join(",") : region;
|
||||
url.searchParams.append("filter[region__in]", regionValue);
|
||||
}
|
||||
|
||||
try {
|
||||
const compliances = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Accept: "application/vnd.api+json",
|
||||
Authorization: `Bearer ${session?.accessToken}`,
|
||||
},
|
||||
});
|
||||
const data = await compliances.json();
|
||||
const parsedData = parseStringify(data);
|
||||
|
||||
revalidatePath("/compliance");
|
||||
return parsedData;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching providers:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
1
ui/actions/compliances/index.ts
Normal file
1
ui/actions/compliances/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./compliances";
|
||||
49
ui/actions/findings/findings.ts
Normal file
49
ui/actions/findings/findings.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { auth } from "@/auth.config";
|
||||
import { parseStringify } from "@/lib";
|
||||
|
||||
export const getFindings = async ({
|
||||
page = 1,
|
||||
query = "",
|
||||
sort = "",
|
||||
filters = {},
|
||||
}) => {
|
||||
const session = await auth();
|
||||
|
||||
if (isNaN(Number(page)) || page < 1)
|
||||
redirect("findings?include=resources.provider,scan");
|
||||
|
||||
const keyServer = process.env.API_BASE_URL;
|
||||
const url = new URL(`${keyServer}/findings?include=resources.provider,scan`);
|
||||
|
||||
if (page) url.searchParams.append("page[number]", page.toString());
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
// Handle multiple filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]") {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const findings = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Accept: "application/vnd.api+json",
|
||||
Authorization: `Bearer ${session?.accessToken}`,
|
||||
},
|
||||
});
|
||||
const data = await findings.json();
|
||||
const parsedData = parseStringify(data);
|
||||
revalidatePath("/findings");
|
||||
return parsedData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching findings:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
1
ui/actions/findings/index.ts
Normal file
1
ui/actions/findings/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./findings";
|
||||
1
ui/actions/invitations/index.ts
Normal file
1
ui/actions/invitations/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./invitation";
|
||||
174
ui/actions/invitations/invitation.ts
Normal file
174
ui/actions/invitations/invitation.ts
Normal file
@@ -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),
|
||||
};
|
||||
}
|
||||
};
|
||||
49
ui/actions/overview/overview.ts
Normal file
49
ui/actions/overview/overview.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
"use server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { auth } from "@/auth.config";
|
||||
import { parseStringify } from "@/lib";
|
||||
|
||||
export const 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;
|
||||
}
|
||||
};
|
||||
1
ui/actions/providers/index.ts
Normal file
1
ui/actions/providers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./providers";
|
||||
322
ui/actions/providers/providers.ts
Normal file
322
ui/actions/providers/providers.ts
Normal file
@@ -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),
|
||||
};
|
||||
}
|
||||
};
|
||||
1
ui/actions/scans/index.ts
Normal file
1
ui/actions/scans/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./scans";
|
||||
156
ui/actions/scans/scans.ts
Normal file
156
ui/actions/scans/scans.ts
Normal file
@@ -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),
|
||||
};
|
||||
}
|
||||
};
|
||||
1
ui/actions/services/index.ts
Normal file
1
ui/actions/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./services";
|
||||
99
ui/actions/services/services.ts
Normal file
99
ui/actions/services/services.ts
Normal file
@@ -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;
|
||||
};
|
||||
1
ui/actions/task/index.ts
Normal file
1
ui/actions/task/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./tasks";
|
||||
24
ui/actions/task/tasks.ts
Normal file
24
ui/actions/task/tasks.ts
Normal file
@@ -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) };
|
||||
}
|
||||
};
|
||||
116
ui/actions/users/users.ts
Normal file
116
ui/actions/users/users.ts
Normal file
@@ -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),
|
||||
};
|
||||
}
|
||||
};
|
||||
60
ui/app/(auth)/layout.tsx
Normal file
60
ui/app/(auth)/layout.tsx
Normal file
@@ -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 (
|
||||
<html suppressHydrationWarning lang="en">
|
||||
<head />
|
||||
<body
|
||||
suppressHydrationWarning
|
||||
className={cn(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
fontSans.variable,
|
||||
)}
|
||||
>
|
||||
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
|
||||
{children}
|
||||
<Toaster />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
7
ui/app/(auth)/sign-in/page.tsx
Normal file
7
ui/app/(auth)/sign-in/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AuthForm } from "@/components/auth/oss";
|
||||
|
||||
const SignIn = () => {
|
||||
return <AuthForm type="sign-in" />;
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
13
ui/app/(auth)/sign-up/page.tsx
Normal file
13
ui/app/(auth)/sign-up/page.tsx
Normal file
@@ -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 <AuthForm type="sign-up" invitationToken={invitationToken} />;
|
||||
};
|
||||
|
||||
export default SignUp;
|
||||
12
ui/app/(prowler)/categories/page.tsx
Normal file
12
ui/app/(prowler)/categories/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
|
||||
import { Header } from "@/components/ui";
|
||||
|
||||
export default async function Categories() {
|
||||
return (
|
||||
<>
|
||||
<Header title="Categories" icon="material-symbols:folder-open-outline" />
|
||||
<Spacer y={4} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
122
ui/app/(prowler)/compliance/page.tsx
Normal file
122
ui/app/(prowler)/compliance/page.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Header title="Compliance" icon="fluent-mdl2:compliance-audit" />
|
||||
<Spacer y={4} />
|
||||
<DataCompliance scans={scanList} regions={regions as string[]} />
|
||||
<Spacer y={12} />
|
||||
<Suspense fallback={<ComplianceSkeletonGrid />}>
|
||||
<SSRComplianceGrid searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-default-500">
|
||||
No compliance data available for the selected scan.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle errors returned by the API
|
||||
if (compliancesData?.errors?.length > 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-default-500">Provide a valid scan ID.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
{compliancesData.data.map((compliance: ComplianceOverviewData) => {
|
||||
const { attributes } = compliance;
|
||||
const {
|
||||
framework,
|
||||
requirements_status: { passed, total },
|
||||
} = attributes;
|
||||
|
||||
return (
|
||||
<ComplianceCard
|
||||
key={compliance.id}
|
||||
title={framework}
|
||||
passingRequirements={passed}
|
||||
totalRequirements={total}
|
||||
prevPassingRequirements={passed}
|
||||
prevTotalRequirements={total}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
36
ui/app/(prowler)/error.tsx
Normal file
36
ui/app/(prowler)/error.tsx
Normal file
@@ -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 (
|
||||
<Alert className="mx-auto mt-[35%] w-fit">
|
||||
<RocketIcon className="h-5 w-5" />
|
||||
<AlertTitle className="text-lg">An unexpected error occurred</AlertTitle>
|
||||
<AlertDescription className="mb-5">
|
||||
We're sorry for the inconvenience. Please try again or contact support
|
||||
if the problem persists.
|
||||
</AlertDescription>
|
||||
<Link href={"/"} className="font-bold">
|
||||
{" "}
|
||||
Go to the homepage
|
||||
</Link>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
174
ui/app/(prowler)/findings/page.tsx
Normal file
174
ui/app/(prowler)/findings/page.tsx
Normal file
@@ -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<string>(
|
||||
allRegionsAndServices
|
||||
.map((item: { region: string }) => item.region)
|
||||
.filter(Boolean) || [],
|
||||
),
|
||||
);
|
||||
const uniqueServices = Array.from(
|
||||
new Set<string>(
|
||||
allRegionsAndServices
|
||||
.map((item: { service: string }) => item.service)
|
||||
.filter(Boolean) || [],
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Findings" icon="ph:list-checks-duotone" />
|
||||
<Spacer />
|
||||
<Spacer y={4} />
|
||||
<FilterControls search date />
|
||||
<Spacer y={8} />
|
||||
<DataTableFilterCustom
|
||||
filters={[
|
||||
...filterFindings,
|
||||
{
|
||||
key: "region__in",
|
||||
labelCheckboxGroup: "Regions",
|
||||
values: uniqueRegions,
|
||||
},
|
||||
{
|
||||
key: "service__in",
|
||||
labelCheckboxGroup: "Services",
|
||||
values: uniqueServices,
|
||||
},
|
||||
{
|
||||
key: "provider_uid__in",
|
||||
labelCheckboxGroup: "Account",
|
||||
values: providerUIDs,
|
||||
},
|
||||
{
|
||||
key: "scan__in",
|
||||
labelCheckboxGroup: "Scans",
|
||||
values: completedScanIds, // Use UUIDs in the filter
|
||||
},
|
||||
]}
|
||||
defaultOpen={true}
|
||||
/>
|
||||
<Spacer y={8} />
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableFindings />}>
|
||||
<SSRDataTable searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<DataTable
|
||||
columns={ColumnFindings}
|
||||
data={expandedResponse?.data || []}
|
||||
metadata={findingsData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
13
ui/app/(prowler)/integrations/page.tsx
Normal file
13
ui/app/(prowler)/integrations/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
import { Header } from "@/components/ui";
|
||||
|
||||
export default function Integrations() {
|
||||
return (
|
||||
<>
|
||||
<Header title="Integrations" icon="tabler:puzzle" />
|
||||
|
||||
<p>Hi hi from Integration page</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonInvitationInfo />}>
|
||||
<SSRDataInvitation searchParams={searchParams} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const SSRDataInvitation = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) => {
|
||||
const invitationId = searchParams.id;
|
||||
|
||||
if (!invitationId) {
|
||||
return <div>Invalid invitation ID</div>;
|
||||
}
|
||||
|
||||
const invitationData = (await getInvitationInfoById(invitationId as string))
|
||||
.data;
|
||||
|
||||
if (!invitationData) {
|
||||
return <div>Invitation not found</div>;
|
||||
}
|
||||
|
||||
const { attributes, links } = invitationData;
|
||||
|
||||
return <InvitationDetails attributes={attributes} selfLink={links.self} />;
|
||||
};
|
||||
32
ui/app/(prowler)/invitations/(send-invite)/layout.tsx
Normal file
32
ui/app/(prowler)/invitations/(send-invite)/layout.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<NavigationHeader
|
||||
title="Send Invitation"
|
||||
icon="icon-park-outline:close-small"
|
||||
href="/invitations"
|
||||
/>
|
||||
<Spacer y={16} />
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-12">
|
||||
<div className="order-1 my-auto hidden h-full lg:col-span-4 lg:col-start-2 lg:block">
|
||||
<WorkflowSendInvite />
|
||||
</div>
|
||||
<div className="order-2 my-auto lg:col-span-5 lg:col-start-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
ui/app/(prowler)/invitations/(send-invite)/new/page.tsx
Normal file
7
ui/app/(prowler)/invitations/(send-invite)/new/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { SendInvitationForm } from "@/components/invitations/workflow/forms/send-invitation-form";
|
||||
|
||||
export default function SendInvitationPage() {
|
||||
return <SendInvitationForm />;
|
||||
}
|
||||
66
ui/app/(prowler)/invitations/page.tsx
Normal file
66
ui/app/(prowler)/invitations/page.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Header title="Invitations" icon="ci:users" />
|
||||
<Spacer y={4} />
|
||||
<FilterControls search />
|
||||
<Spacer y={8} />
|
||||
<SendInvitationButton />
|
||||
<Spacer y={4} />
|
||||
<DataTableFilterCustom filters={filterInvitations || []} />
|
||||
<Spacer y={8} />
|
||||
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableInvitation />}>
|
||||
<SSRDataTable searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<DataTable
|
||||
columns={ColumnsInvitation}
|
||||
data={invitationsData?.data || []}
|
||||
metadata={invitationsData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
58
ui/app/(prowler)/layout.tsx
Normal file
58
ui/app/(prowler)/layout.tsx
Normal file
@@ -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 (
|
||||
<html suppressHydrationWarning lang="en">
|
||||
<head />
|
||||
<body
|
||||
suppressHydrationWarning
|
||||
className={cn(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
fontSans.variable,
|
||||
)}
|
||||
>
|
||||
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
|
||||
<div className="flex h-dvh items-center justify-center overflow-hidden">
|
||||
<SidebarWrap />
|
||||
<main className="no-scrollbar mb-auto h-full flex-1 flex-col overflow-y-auto px-6 py-4 xl:px-10">
|
||||
{children}
|
||||
<Toaster />
|
||||
</main>
|
||||
</div>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
42
ui/app/(prowler)/page.tsx
Normal file
42
ui/app/(prowler)/page.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Header title="Scan Overview" icon="solar:pie-chart-2-outline" />
|
||||
<Spacer y={4} />
|
||||
<div className="min-h-screen">
|
||||
<div className="container mx-auto space-y-8 px-0 py-6">
|
||||
{/* Providers Overview */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<Suspense fallback={<SkeletonProvidersOverview />}>
|
||||
<SSRProvidersOverview />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2"></div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const SSRProvidersOverview = async () => {
|
||||
const providersOverview = await getProvidersOverview({});
|
||||
|
||||
if (!providersOverview) {
|
||||
return <p>There is no providers overview info available</p>;
|
||||
}
|
||||
|
||||
return <ProvidersOverview providersOverview={providersOverview} />;
|
||||
};
|
||||
30
ui/app/(prowler)/profile/page.tsx
Normal file
30
ui/app/(prowler)/profile/page.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Header title="User Profile" icon="ci:users" />
|
||||
<Spacer y={4} />
|
||||
<Spacer y={6} />
|
||||
<pre>{JSON.stringify(session.user, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(session.userId, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(session.tenantId, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 && <ViaCredentialsForm searchParams={searchParams} />}
|
||||
{useRoleForm && <ViaRoleForm searchParams={searchParams} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { ConnectAccountForm } from "@/components/providers/workflow/forms";
|
||||
|
||||
export default function ConnectAccountPage() {
|
||||
return <ConnectAccountForm />;
|
||||
}
|
||||
@@ -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 (
|
||||
<LaunchScanForm searchParams={searchParams} providerData={providerData} />
|
||||
);
|
||||
}
|
||||
32
ui/app/(prowler)/providers/(set-up-provider)/layout.tsx
Normal file
32
ui/app/(prowler)/providers/(set-up-provider)/layout.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<NavigationHeader
|
||||
title="Connect your cloud account"
|
||||
icon="icon-park-outline:close-small"
|
||||
href="/providers"
|
||||
/>
|
||||
<Spacer y={16} />
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-12">
|
||||
<div className="order-1 my-auto hidden h-full lg:col-span-4 lg:col-start-2 lg:block">
|
||||
<WorkflowAddProvider />
|
||||
</div>
|
||||
<div className="order-2 my-auto lg:col-span-5 lg:col-start-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<SSRTestConnection searchParams={searchParams} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<TestConnectionForm
|
||||
searchParams={searchParams}
|
||||
providerData={providerData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
65
ui/app/(prowler)/providers/page.tsx
Normal file
65
ui/app/(prowler)/providers/page.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Header title="Providers" icon="fluent:cloud-sync-24-regular" />
|
||||
|
||||
<Spacer y={4} />
|
||||
<FilterControls search providers />
|
||||
<Spacer y={8} />
|
||||
<AddProvider />
|
||||
<Spacer y={4} />
|
||||
<DataTableFilterCustom filters={filterProviders || []} />
|
||||
<Spacer y={8} />
|
||||
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableProviders />}>
|
||||
<SSRDataTable searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<DataTable
|
||||
columns={ColumnProviders}
|
||||
data={providersData?.data || []}
|
||||
metadata={providersData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
104
ui/app/(prowler)/scans/page.tsx
Normal file
104
ui/app/(prowler)/scans/page.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Header title="Scans" icon="lucide:scan-search" />
|
||||
|
||||
<Spacer y={4} />
|
||||
<LaunchScanWorkflow providers={providerInfo} />
|
||||
<Spacer y={8} />
|
||||
<div className="flex flex-row justify-between">
|
||||
<DataTableFilterCustom filters={filterScans || []} />
|
||||
<ButtonRefreshData
|
||||
onPress={async () => {
|
||||
"use server";
|
||||
await getScans({});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Spacer y={8} />
|
||||
|
||||
<div className="grid grid-cols-12 items-start gap-4">
|
||||
<div className="col-span-12">
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableScans />}>
|
||||
<SSRDataTableScans searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<DataTable
|
||||
columns={ColumnGetScans}
|
||||
data={scansData?.data || []}
|
||||
metadata={scansData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// const getExecutingScans = async () => {
|
||||
// const scansData = await getScans({});
|
||||
|
||||
// return scansData?.data?.some(
|
||||
// (scan: ScanProps) =>
|
||||
// scan.attributes.state === "executing" && scan.attributes.progress < 100,
|
||||
// );
|
||||
// };
|
||||
51
ui/app/(prowler)/services/page.tsx
Normal file
51
ui/app/(prowler)/services/page.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Header
|
||||
title="Services"
|
||||
icon="material-symbols:linked-services-outline"
|
||||
/>
|
||||
<Spacer y={4} />
|
||||
<FilterControls />
|
||||
<Spacer y={4} />
|
||||
<Suspense key={searchParamsKey} fallback={<ServiceSkeletonGrid />}>
|
||||
<SSRServiceGrid searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const SSRServiceGrid = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) => {
|
||||
const servicesData = await getServices(searchParams);
|
||||
const [services] = await Promise.all([servicesData]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{services?.map((service: any) => (
|
||||
<ServiceCard
|
||||
key={service.service_id}
|
||||
fidingsFailed={service.fail_findings}
|
||||
serviceAlias={service.service_alias}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
12
ui/app/(prowler)/settings/page.tsx
Normal file
12
ui/app/(prowler)/settings/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
|
||||
import { Header } from "@/components/ui";
|
||||
|
||||
export default async function Settings() {
|
||||
return (
|
||||
<>
|
||||
<Header title="Settings" icon="solar:settings-outline" />
|
||||
<Spacer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
63
ui/app/(prowler)/users/page.tsx
Normal file
63
ui/app/(prowler)/users/page.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Header title="Users" icon="ci:users" />
|
||||
<Spacer y={4} />
|
||||
<FilterControls search />
|
||||
<Spacer y={8} />
|
||||
<AddUserButton />
|
||||
<Spacer y={4} />
|
||||
<DataTableFilterCustom filters={filterUsers || []} />
|
||||
<Spacer y={8} />
|
||||
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableUser />}>
|
||||
<SSRDataTable searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<DataTable
|
||||
columns={ColumnsUser}
|
||||
data={usersData?.data || []}
|
||||
metadata={usersData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
12
ui/app/(prowler)/workloads/page.tsx
Normal file
12
ui/app/(prowler)/workloads/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
|
||||
import { Header } from "@/components/ui";
|
||||
|
||||
export default async function Workloads() {
|
||||
return (
|
||||
<>
|
||||
<Header title="Workloads" icon="lucide:tags" />
|
||||
<Spacer y={4} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
ui/app/api/auth/[...nextauth]/route.ts
Normal file
3
ui/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "@/auth.config";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
10
ui/app/api/services/route.ts
Normal file
10
ui/app/api/services/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
25
ui/app/providers.tsx
Normal file
25
ui/app/providers.tsx
Normal file
@@ -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 (
|
||||
<SessionProvider>
|
||||
<NextUIProvider navigate={router.push}>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
</NextUIProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
185
ui/auth.config.ts
Normal file
185
ui/auth.config.ts
Normal file
@@ -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);
|
||||
82
ui/components/ThemeSwitch.tsx
Normal file
82
ui/components/ThemeSwitch.tsx
Normal file
@@ -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<ThemeSwitchProps> = ({
|
||||
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 (
|
||||
<Component
|
||||
{...getBaseProps({
|
||||
className: clsx(
|
||||
"px-px transition-opacity hover:opacity-80 cursor-pointer",
|
||||
className,
|
||||
classNames?.base,
|
||||
),
|
||||
})}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<input {...getInputProps()} />
|
||||
</VisuallyHidden>
|
||||
<div
|
||||
{...getWrapperProps()}
|
||||
className={slots.wrapper({
|
||||
class: clsx(
|
||||
[
|
||||
"h-auto w-auto",
|
||||
"bg-transparent",
|
||||
"rounded-lg",
|
||||
"flex items-center justify-center",
|
||||
"group-data-[selected=true]:bg-transparent",
|
||||
"!text-default-500",
|
||||
"pt-px",
|
||||
"px-0",
|
||||
"mx-0",
|
||||
],
|
||||
classNames?.wrapper,
|
||||
),
|
||||
})}
|
||||
>
|
||||
{!isSelected || isSSR ? (
|
||||
<SunFilledIcon size={22} />
|
||||
) : (
|
||||
<MoonFilledIcon size={22} />
|
||||
)}
|
||||
</div>
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
318
ui/components/auth/oss/auth-form.tsx
Normal file
318
ui/components/auth/oss/auth-form.tsx
Normal file
@@ -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<z.infer<typeof formSchema>>({
|
||||
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<typeof formSchema>) => {
|
||||
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 (
|
||||
<div className="relative flex h-screen w-screen">
|
||||
{/* Auth Form */}
|
||||
<div className="relative flex w-full items-center justify-center lg:w-full">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute h-full w-full bg-[radial-gradient(#6af400_1px,transparent_1px)] [background-size:16px_16px] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_10%,transparent_80%)]"></div>
|
||||
|
||||
<div className="relative z-10 flex w-full max-w-sm flex-col gap-4 rounded-large border-1 border-divider bg-white/90 px-8 py-10 shadow-small dark:bg-background/85 md:max-w-md">
|
||||
{/* Prowler Logo */}
|
||||
<div className="absolute -top-[100px] left-1/2 z-10 flex h-fit w-fit -translate-x-1/2">
|
||||
<ProwlerExtended width={300} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="pb-2 text-xl font-medium">
|
||||
{type === "sign-in" ? "Sign In" : "Sign Up"}
|
||||
</p>
|
||||
<ThemeSwitch aria-label="Toggle theme" />
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-3"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
{type === "sign-up" && (
|
||||
<>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="name"
|
||||
type="text"
|
||||
label="Name"
|
||||
placeholder="Enter your name"
|
||||
isInvalid={!!form.formState.errors.name}
|
||||
/>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="company"
|
||||
type="text"
|
||||
label="Company Name"
|
||||
placeholder="Enter your company name"
|
||||
isRequired={false}
|
||||
isInvalid={!!form.formState.errors.company}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="Enter your email"
|
||||
isInvalid={!!form.formState.errors.email}
|
||||
/>
|
||||
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="password"
|
||||
password
|
||||
isInvalid={!!form.formState.errors.password}
|
||||
/>
|
||||
|
||||
{type === "sign-in" && (
|
||||
<div className="flex items-center justify-between px-1 py-2">
|
||||
<Checkbox name="remember" size="sm">
|
||||
Remember me
|
||||
</Checkbox>
|
||||
<Link className="text-default-500" href="#">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{type === "sign-up" && (
|
||||
<>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
confirmPassword
|
||||
/>
|
||||
{invitationToken && (
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="invitationToken"
|
||||
type="text"
|
||||
label="Invitation Token"
|
||||
placeholder={invitationToken}
|
||||
defaultValue={invitationToken}
|
||||
isRequired={false}
|
||||
isInvalid={!!form.formState.errors.invitationToken}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="termsAndConditions"
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
isRequired
|
||||
className="py-4"
|
||||
size="sm"
|
||||
checked={field.value}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
>
|
||||
I agree with the
|
||||
<Link href="#" size="sm">
|
||||
Terms
|
||||
</Link>
|
||||
and
|
||||
<Link href="#" size="sm">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</Checkbox>
|
||||
</FormControl>
|
||||
<FormMessage className="text-system-error dark:text-system-error" />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{form.formState.errors?.email && (
|
||||
<div className="flex flex-row items-center gap-2 text-system-error">
|
||||
<NotificationIcon size={16} />
|
||||
<p className="text-s">No user found</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel={type === "sign-in" ? "Log In" : "Sign Up"}
|
||||
ariaDisabled={isLoading}
|
||||
className="w-full"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="md"
|
||||
radius="md"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span>Loading</span>
|
||||
) : (
|
||||
<span>{type === "sign-in" ? "Log In" : "Sign Up"}</span>
|
||||
)}
|
||||
</CustomButton>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{type === "sign-in" && (
|
||||
<>
|
||||
<div className="flex items-center gap-4 py-2">
|
||||
<Divider className="flex-1" />
|
||||
<p className="shrink-0 text-tiny text-default-500">OR</p>
|
||||
<Divider className="flex-1" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
startContent={
|
||||
<Icon icon="flat-color-icons:google" width={24} />
|
||||
}
|
||||
variant="bordered"
|
||||
>
|
||||
Continue with Google
|
||||
</Button>
|
||||
<Button
|
||||
startContent={
|
||||
<Icon
|
||||
className="text-default-500"
|
||||
icon="fe:github"
|
||||
width={24}
|
||||
/>
|
||||
}
|
||||
variant="bordered"
|
||||
>
|
||||
Continue with Github
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{type === "sign-in" ? (
|
||||
<p className="text-center text-small">
|
||||
Need to create an account?
|
||||
<Link href="/sign-up">Sign Up</Link>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-center text-small">
|
||||
Already have an account?
|
||||
<Link href="/sign-in">Log In</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
ui/components/auth/oss/index.ts
Normal file
1
ui/components/auth/oss/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./auth-form";
|
||||
76
ui/components/charts/SeverityChart.tsx
Normal file
76
ui/components/charts/SeverityChart.tsx
Normal file
@@ -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 (
|
||||
<div className="my-auto">
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart accessibilityLayer data={chartData} layout="vertical">
|
||||
<YAxis
|
||||
dataKey="severity"
|
||||
type="category"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) =>
|
||||
chartConfig[value as keyof typeof chartConfig]?.label
|
||||
}
|
||||
/>
|
||||
<XAxis dataKey="findings" type="number" hide>
|
||||
<LabelList position="insideTop" offset={12} fontSize={12} />
|
||||
</XAxis>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" />}
|
||||
/>
|
||||
|
||||
<Bar dataKey="findings" layout="vertical" radius={12}>
|
||||
<LabelList
|
||||
position="insideRight"
|
||||
offset={10}
|
||||
className="fill-foreground font-bold"
|
||||
fontSize={12}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
148
ui/components/charts/StatusChart.tsx
Normal file
148
ui/components/charts/StatusChart.tsx
Normal file
@@ -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 (
|
||||
<div className="my-auto flex items-center justify-center self-center">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square min-w-[200px] md:min-h-[250px]"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<Pie
|
||||
data={chartData}
|
||||
dataKey="number"
|
||||
nameKey="findings"
|
||||
innerRadius={60}
|
||||
strokeWidth={50}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
radius={70}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="fill-foreground text-3xl font-bold"
|
||||
>
|
||||
{totalVisitors.toLocaleString()}
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 24}
|
||||
className="fill-foreground"
|
||||
>
|
||||
Findings
|
||||
</tspan>
|
||||
</text>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
<div className="mx-6 flex flex-col justify-center gap-2 text-small">
|
||||
<div className="flex space-x-4">
|
||||
<Chip
|
||||
className="h-5"
|
||||
variant="flat"
|
||||
startContent={<SuccessIcon size={18} />}
|
||||
color="success"
|
||||
radius="lg"
|
||||
size="md"
|
||||
>
|
||||
{chartData[0].number}
|
||||
</Chip>
|
||||
<Divider orientation="vertical" />
|
||||
<span>{updatedChartData[0].percent}</span>
|
||||
<Divider orientation="vertical" />
|
||||
</div>
|
||||
<div className="flex items-center font-light leading-none">
|
||||
No change from last scan
|
||||
</div>
|
||||
<Spacer y={4} />
|
||||
<div className="text-muted-foreground flex flex-col gap-2 leading-none">
|
||||
<div className="flex space-x-4">
|
||||
<Chip
|
||||
className="h-5"
|
||||
variant="flat"
|
||||
startContent={<NotificationIcon size={18} />}
|
||||
color="danger"
|
||||
radius="lg"
|
||||
size="md"
|
||||
>
|
||||
{chartData[1].number}
|
||||
</Chip>
|
||||
<Divider orientation="vertical" />
|
||||
<span>{updatedChartData[1].percent}</span>
|
||||
<Divider orientation="vertical" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1 font-medium leading-none">
|
||||
+2 findings from last scan <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
ui/components/charts/index.ts
Normal file
2
ui/components/charts/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./SeverityChart";
|
||||
export * from "./StatusChart";
|
||||
83
ui/components/compliance/compliance-card.tsx
Normal file
83
ui/components/compliance/compliance-card.tsx
Normal file
@@ -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<ComplianceCardProps> = ({
|
||||
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 (
|
||||
<Card fullWidth isPressable isHoverable shadow="sm">
|
||||
<CardBody className="flex flex-row items-center justify-between space-x-4 dark:bg-prowler-blue-800">
|
||||
<div className="flex w-full items-center space-x-4">
|
||||
<Image
|
||||
src={getComplianceIcon(title)}
|
||||
alt={`${title} logo`}
|
||||
className="h-10 w-10 min-w-10 rounded-md border-1 border-gray-300 bg-white object-contain p-1"
|
||||
/>
|
||||
<div className="flex w-full flex-col">
|
||||
<h4 className="text-md font-bold leading-5 3xl:text-lg">{title}</h4>
|
||||
<Progress
|
||||
label="Your Rating:"
|
||||
size="sm"
|
||||
aria-label="Your Rating"
|
||||
value={ratingPercentage}
|
||||
showValueLabel={true}
|
||||
className="mt-2 font-semibold"
|
||||
color={getRatingColor(ratingPercentage)}
|
||||
/>
|
||||
<div className="mt-2 flex justify-between">
|
||||
<small>
|
||||
<span className="mr-1 font-semibold">
|
||||
{passingRequirements} / {totalRequirements}
|
||||
</span>
|
||||
Passing Requirements
|
||||
</small>
|
||||
{/* <small>{getScanChange()}</small> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
18
ui/components/compliance/compliance-skeleton-grid.tsx
Normal file
18
ui/components/compliance/compliance-skeleton-grid.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Card, Skeleton } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
export const ComplianceSkeletonGrid = () => {
|
||||
return (
|
||||
<Card className="h-fit w-full p-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 3xl:grid-cols-4">
|
||||
{[...Array(28)].map((_, index) => (
|
||||
<div key={index} className="flex flex-col space-y-4">
|
||||
<Skeleton className="h-28 rounded-lg">
|
||||
<div className="h-full bg-default-300"></div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
90
ui/components/compliance/data-compliance/data-compliance.tsx
Normal file
90
ui/components/compliance/data-compliance/data-compliance.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { SelectScanComplianceData } from "@/components/compliance/data-compliance";
|
||||
import { CrossIcon } from "@/components/icons";
|
||||
import { CustomButton, CustomDropdownFilter } from "@/components/ui/custom";
|
||||
|
||||
interface DataComplianceProps {
|
||||
scans: { id: string; name: string; state: string; progress: number }[];
|
||||
regions: string[];
|
||||
}
|
||||
|
||||
export const DataCompliance = ({ scans, regions }: DataComplianceProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [showClearButton, setShowClearButton] = useState(false);
|
||||
const scanIdParam = searchParams.get("scanId");
|
||||
const selectedScanId = scanIdParam || scans[0]?.id;
|
||||
|
||||
useEffect(() => {
|
||||
const hasFilters = Array.from(searchParams.keys()).some(
|
||||
(key) => key.startsWith("filter[") || key === "sort",
|
||||
);
|
||||
setShowClearButton(hasFilters);
|
||||
}, [searchParams]);
|
||||
const handleScanChange = (selectedKey: string) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("scanId", selectedKey);
|
||||
router.push(`?${params.toString()}`);
|
||||
};
|
||||
|
||||
const pushDropdownFilter = useCallback(
|
||||
(key: string, values: string[]) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
const filterKey = `filter[${key}]`;
|
||||
|
||||
if (values.length === 0) {
|
||||
params.delete(filterKey);
|
||||
} else {
|
||||
params.set(filterKey, values.join(","));
|
||||
}
|
||||
|
||||
router.push(`?${params.toString()}`);
|
||||
},
|
||||
[router, searchParams],
|
||||
);
|
||||
const clearAllFilters = useCallback(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
Array.from(params.keys()).forEach((key) => {
|
||||
if (key.startsWith("filter[") || key === "sort") {
|
||||
params.delete(key);
|
||||
}
|
||||
});
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
}, [router, searchParams]);
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-1 items-center gap-x-4 gap-y-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<SelectScanComplianceData
|
||||
scans={scans}
|
||||
selectedScanId={selectedScanId}
|
||||
onSelectionChange={handleScanChange}
|
||||
/>
|
||||
<CustomDropdownFilter
|
||||
filter={{
|
||||
key: "region__in",
|
||||
values: regions,
|
||||
labelCheckboxGroup: "Regions",
|
||||
}}
|
||||
onFilterChange={pushDropdownFilter}
|
||||
/>
|
||||
{showClearButton && (
|
||||
<CustomButton
|
||||
ariaLabel="Reset"
|
||||
className="w-full md:w-fit"
|
||||
onPress={clearAllFilters}
|
||||
variant="dashed"
|
||||
size="sm"
|
||||
endContent={<CrossIcon size={24} />}
|
||||
radius="sm"
|
||||
>
|
||||
Reset
|
||||
</CustomButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
2
ui/components/compliance/data-compliance/index.ts
Normal file
2
ui/components/compliance/data-compliance/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./data-compliance";
|
||||
export * from "./select-scan-compliance-data";
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Select, SelectItem } from "@nextui-org/react";
|
||||
|
||||
interface SelectScanComplianceDataProps {
|
||||
scans: { id: string; name: string; state: string; progress: number }[];
|
||||
selectedScanId: string;
|
||||
onSelectionChange: (selectedKey: string) => void;
|
||||
}
|
||||
|
||||
export const SelectScanComplianceData = ({
|
||||
scans,
|
||||
selectedScanId,
|
||||
onSelectionChange,
|
||||
}: SelectScanComplianceDataProps) => {
|
||||
return (
|
||||
<Select
|
||||
aria-label="Select a Scan"
|
||||
placeholder="Select a scan"
|
||||
labelPlacement="outside"
|
||||
size="md"
|
||||
selectedKeys={new Set([selectedScanId])}
|
||||
onSelectionChange={(keys) =>
|
||||
onSelectionChange(Array.from(keys)[0] as string)
|
||||
}
|
||||
renderValue={() => {
|
||||
const selectedItem = scans.find((item) => item.id === selectedScanId);
|
||||
return selectedItem ? (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold">{selectedItem.name}</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
State: {selectedItem.state}, Progress: {selectedItem.progress}%
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
"Select a scan"
|
||||
);
|
||||
}}
|
||||
>
|
||||
{scans.map((scan) => (
|
||||
<SelectItem key={scan.id} textValue={scan.name}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold">{scan.name}</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
State: {scan.state}, Progress: {scan.progress}%
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
2
ui/components/compliance/index.ts
Normal file
2
ui/components/compliance/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./compliance-card";
|
||||
export * from "./compliance-skeleton-grid";
|
||||
32
ui/components/filters/custom-account-selection.tsx
Normal file
32
ui/components/filters/custom-account-selection.tsx
Normal file
@@ -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 (
|
||||
<Select
|
||||
label="Account"
|
||||
aria-label="Select an Account"
|
||||
placeholder="Select an account"
|
||||
selectionMode="multiple"
|
||||
className="w-full"
|
||||
size="sm"
|
||||
>
|
||||
{accounts.map((acc) => (
|
||||
<SelectItem key={acc.key}>{acc.label}</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
15
ui/components/filters/custom-checkbox-muted-findings.tsx
Normal file
15
ui/components/filters/custom-checkbox-muted-findings.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Checkbox } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
export const CustomCheckboxMutedFindings = () => {
|
||||
return (
|
||||
<Checkbox
|
||||
className="xl:-mt-8"
|
||||
size="md"
|
||||
color="danger"
|
||||
aria-label="Include Muted Findings"
|
||||
>
|
||||
Include Muted Findings
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
97
ui/components/filters/custom-date-picker.tsx
Normal file
97
ui/components/filters/custom-date-picker.tsx
Normal file
@@ -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 (
|
||||
<div className="flex w-full flex-col md:gap-2">
|
||||
<DatePicker
|
||||
aria-label="Select a Date"
|
||||
CalendarTopContent={
|
||||
<ButtonGroup
|
||||
fullWidth
|
||||
className="bg-content1 px-3 pb-2 pt-3 dark:bg-prowler-blue-400 [&>button]:border-default-200/60 [&>button]:text-default-500"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
>
|
||||
<Button onPress={() => handleDateChange(now)}>Today</Button>
|
||||
<Button onPress={() => handleDateChange(nextWeek)}>
|
||||
Next week
|
||||
</Button>
|
||||
<Button onPress={() => handleDateChange(nextMonth)}>
|
||||
Next month
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
}
|
||||
calendarProps={{
|
||||
focusedValue: value,
|
||||
onFocusChange: setValue,
|
||||
nextButtonProps: {
|
||||
variant: "bordered",
|
||||
},
|
||||
prevButtonProps: {
|
||||
variant: "bordered",
|
||||
},
|
||||
}}
|
||||
value={value}
|
||||
onChange={handleDateChange}
|
||||
size="sm"
|
||||
variant="flat"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
44
ui/components/filters/custom-provider-inputs.tsx
Normal file
44
ui/components/filters/custom-provider-inputs.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
AWSProviderBadge,
|
||||
AzureProviderBadge,
|
||||
GCPProviderBadge,
|
||||
KS8ProviderBadge,
|
||||
} from "../icons/providers-badge";
|
||||
|
||||
export const CustomProviderInputAWS = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<AWSProviderBadge width={25} height={25} />
|
||||
<p className="text-sm">Amazon Web Services</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomProviderInputAzure = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<AzureProviderBadge width={25} height={25} />
|
||||
<p className="text-sm">Azure</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomProviderInputGCP = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<GCPProviderBadge width={25} height={25} />
|
||||
<p className="text-sm">Google Cloud Platform</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomProviderInputKubernetes = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<KS8ProviderBadge width={25} height={25} />
|
||||
<p className="text-sm">Kubernetes</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
51
ui/components/filters/custom-region-selection.tsx
Normal file
51
ui/components/filters/custom-region-selection.tsx
Normal file
@@ -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 (
|
||||
<Select
|
||||
label="Region"
|
||||
aria-label="Select a Region"
|
||||
placeholder="Select a region"
|
||||
selectionMode="multiple"
|
||||
className="w-full"
|
||||
size="sm"
|
||||
>
|
||||
{regions.map((acc) => (
|
||||
<SelectItem key={acc.key}>{acc.label}</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
62
ui/components/filters/custom-search-input.tsx
Normal file
62
ui/components/filters/custom-search-input.tsx
Normal file
@@ -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 (
|
||||
<Input
|
||||
variant="flat"
|
||||
aria-label="Search"
|
||||
placeholder="Search..."
|
||||
labelPlacement="outside"
|
||||
value={searchQuery}
|
||||
startContent={<SearchIcon className="text-default-400" width={16} />}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSearchQuery(value);
|
||||
debouncedChangeHandler(value);
|
||||
}}
|
||||
endContent={
|
||||
searchQuery && (
|
||||
<button onClick={clearIconSearch} className="focus:outline-none">
|
||||
<XCircle className="h-4 w-4 text-default-400" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
radius="sm"
|
||||
size="sm"
|
||||
/>
|
||||
);
|
||||
};
|
||||
91
ui/components/filters/custom-select-provider.tsx
Normal file
91
ui/components/filters/custom-select-provider.tsx
Normal file
@@ -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: <CustomProviderInputAWS />,
|
||||
},
|
||||
{
|
||||
key: "gcp",
|
||||
label: "Google Cloud Platform",
|
||||
value: <CustomProviderInputGCP />,
|
||||
},
|
||||
{
|
||||
key: "azure",
|
||||
label: "Microsoft Azure",
|
||||
value: <CustomProviderInputAzure />,
|
||||
},
|
||||
{
|
||||
key: "kubernetes",
|
||||
label: "Kubernetes",
|
||||
value: <CustomProviderInputKubernetes />,
|
||||
},
|
||||
];
|
||||
|
||||
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 (
|
||||
<Select
|
||||
items={dataInputsProvider}
|
||||
aria-label="Select a Provider"
|
||||
placeholder="Select a provider"
|
||||
labelPlacement="outside"
|
||||
size="sm"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
applyProviderFilter(value);
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
renderValue={(items) => {
|
||||
return items.map((item) => (
|
||||
<div key={item.key} className="flex items-center gap-2">
|
||||
{item.data?.value}
|
||||
</div>
|
||||
));
|
||||
}}
|
||||
>
|
||||
{(item) => (
|
||||
<SelectItem key={item.key} textValue={item.key} aria-label={item.label}>
|
||||
<div className="flex items-center gap-2">{item.value}</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
74
ui/components/filters/data-filters.ts
Normal file
74
ui/components/filters/data-filters.ts
Normal file
@@ -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"],
|
||||
},
|
||||
];
|
||||
75
ui/components/filters/filter-controls.tsx
Normal file
75
ui/components/filters/filter-controls.tsx
Normal file
@@ -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<FilterControlsProps> = ({
|
||||
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 (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-1 items-center gap-x-4 gap-y-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{search && <CustomSearchInput />}
|
||||
{providers && <CustomSelectProvider />}
|
||||
{date && <CustomDatePicker />}
|
||||
{regions && <CustomRegionSelection />}
|
||||
{accounts && <CustomAccountSelection />}
|
||||
{mutedFindings && <CustomCheckboxMutedFindings />}
|
||||
|
||||
{showClearButton && (
|
||||
<CustomButton
|
||||
ariaLabel="Reset"
|
||||
className="w-full md:w-fit"
|
||||
onPress={clearAllFilters}
|
||||
variant="dashed"
|
||||
size="sm"
|
||||
endContent={<CrossIcon size={24} />}
|
||||
radius="sm"
|
||||
>
|
||||
Reset
|
||||
</CustomButton>
|
||||
)}
|
||||
</div>
|
||||
{customFilters && <DataTableFilterCustom filters={customFilters} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
8
ui/components/filters/index.ts
Normal file
8
ui/components/filters/index.ts
Normal file
@@ -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";
|
||||
177
ui/components/findings/table/column-findings.tsx
Normal file
177
ui/components/findings/table/column-findings.tsx
Normal file
@@ -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<FindingProps>[] = [
|
||||
{
|
||||
accessorKey: "check",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={"Check"} param="check_id" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { checktitle } = getFindingsMetadata(row);
|
||||
return <p className="max-w-96 truncate text-small">{checktitle}</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "severity",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={"Severity"}
|
||||
param="severity"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { severity },
|
||||
} = getFindingsData(row);
|
||||
return <SeverityBadge severity={severity} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={"Status"} param="status" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { status },
|
||||
} = getFindingsData(row);
|
||||
|
||||
return <StatusFindingBadge size="sm" status={status} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "scanName",
|
||||
header: "Scan Name",
|
||||
cell: ({ row }) => {
|
||||
const name = getScanData(row, "name");
|
||||
|
||||
return (
|
||||
<p className="text-small">
|
||||
{typeof name === "string" || typeof name === "number"
|
||||
? name
|
||||
: "Invalid data"}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "region",
|
||||
header: "Region",
|
||||
cell: ({ row }) => {
|
||||
const region = getResourceData(row, "region");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{typeof region === "string" ? region : "Invalid region"}</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "service",
|
||||
header: "Service",
|
||||
cell: ({ row }) => {
|
||||
const { servicename } = getFindingsMetadata(row);
|
||||
return <p className="max-w-96 truncate text-small">{servicename}</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "account",
|
||||
header: "Account",
|
||||
cell: ({ row }) => {
|
||||
const account = getProviderData(row, "uid");
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="max-w-96 truncate text-small">
|
||||
{typeof account === "string" ? account : "Invalid account"}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "moreInfo",
|
||||
header: "Details",
|
||||
cell: ({ row }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const findingId = searchParams.get("id");
|
||||
const isOpen = findingId === row.original.id;
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<TriggerSheet
|
||||
triggerComponent={<InfoIcon className="text-primary" size={16} />}
|
||||
title="Finding Details"
|
||||
description="View the finding details"
|
||||
defaultOpen={isOpen}
|
||||
>
|
||||
<DataTableRowDetails
|
||||
entityId={row.original.id}
|
||||
findingDetails={row.original}
|
||||
/>
|
||||
</TriggerSheet>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return <DataTableRowActions row={row} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
99
ui/components/findings/table/data-table-row-actions.tsx
Normal file
99
ui/components/findings/table/data-table-row-actions.tsx
Normal file
@@ -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<FindingProps> {
|
||||
row: Row<FindingProps>;
|
||||
}
|
||||
const iconClasses =
|
||||
"text-2xl text-default-500 pointer-events-none flex-shrink-0";
|
||||
|
||||
export function DataTableRowActions<FindingProps>({
|
||||
row,
|
||||
}: DataTableRowActionsProps<FindingProps>) {
|
||||
const findingId = (row.original as { id: string }).id;
|
||||
return (
|
||||
<>
|
||||
{/* <CustomAlertModal
|
||||
isOpen={isEditOpen}
|
||||
onOpenChange={setIsEditOpen}
|
||||
title="Edit Provider"
|
||||
description={"Edit the provider details"}
|
||||
>
|
||||
<EditForm
|
||||
providerId={providerId}
|
||||
providerAlias={providerAlias}
|
||||
setIsOpen={setIsEditOpen}
|
||||
/>
|
||||
</CustomAlertModal>
|
||||
<CustomAlertModal
|
||||
isOpen={isDeleteOpen}
|
||||
onOpenChange={setIsDeleteOpen}
|
||||
title="Are you absolutely sure?"
|
||||
description="This action cannot be undone. This will permanently delete your provider account and remove your data from the server."
|
||||
>
|
||||
<DeleteForm providerId={providerId} setIsOpen={setIsDeleteOpen} />
|
||||
</CustomAlertModal> */}
|
||||
|
||||
<div className="relative flex items-center justify-end gap-2">
|
||||
<Dropdown
|
||||
className="shadow-xl dark:bg-prowler-blue-800"
|
||||
placement="bottom"
|
||||
>
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly radius="full" size="sm" variant="light">
|
||||
<VerticalDotsIcon className="text-default-400" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
closeOnSelect
|
||||
aria-label="Actions"
|
||||
color="default"
|
||||
variant="flat"
|
||||
>
|
||||
<DropdownSection title="Actions">
|
||||
<DropdownItem
|
||||
key="jira"
|
||||
description="Allows you to send the finding to Jira"
|
||||
textValue="Send to Jira"
|
||||
startContent={<EditDocumentBulkIcon className={iconClasses} />}
|
||||
// onClick={() => setIsEditOpen(true)}
|
||||
>
|
||||
<span className="hidden text-sm">{findingId}</span>
|
||||
Send to Jira
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="slack"
|
||||
description="Allows you to send the finding to Slack"
|
||||
textValue="Send to Slack"
|
||||
startContent={<EditDocumentBulkIcon className={iconClasses} />}
|
||||
// onClick={() => setIsEditOpen(true)}
|
||||
>
|
||||
Send to Slack
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
40
ui/components/findings/table/data-table-row-details.tsx
Normal file
40
ui/components/findings/table/data-table-row-details.tsx
Normal file
@@ -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 <FindingDetail findingDetails={findingDetails} />;
|
||||
};
|
||||
219
ui/components/findings/table/finding-detail.tsx
Normal file
219
ui/components/findings/table/finding-detail.tsx
Normal file
@@ -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 (
|
||||
<div className="flex flex-col gap-6 rounded-lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="line-clamp-2 text-xl font-bold leading-tight text-gray-800 dark:text-prowler-theme-pale/90">
|
||||
{attributes.check_metadata.checktitle}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-prowler-theme-pale/70">
|
||||
{resource.service}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={`rounded-lg px-3 py-1 text-sm font-semibold ${
|
||||
attributes.status === "PASS"
|
||||
? "bg-green-100 text-green-600"
|
||||
: attributes.status === "MANUAL"
|
||||
? "bg-gray-100 text-gray-600"
|
||||
: "bg-red-100 text-red-600"
|
||||
}`}
|
||||
>
|
||||
{attributes.status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Check Metadata */}
|
||||
<div className="flex flex-col gap-4 rounded-lg p-4 shadow dark:bg-prowler-blue-400">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-gray-800 dark:text-prowler-theme-pale/90">
|
||||
Check Metadata
|
||||
</h3>
|
||||
<SeverityBadge severity={attributes.severity} />
|
||||
</div>
|
||||
{attributes.status === "FAIL" && (
|
||||
<Snippet
|
||||
className="max-w-full py-4"
|
||||
color="danger"
|
||||
hideCopyButton
|
||||
hideSymbol
|
||||
>
|
||||
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
|
||||
Risk
|
||||
</p>
|
||||
<p className="whitespace-pre-line text-gray-800 dark:text-prowler-theme-pale/90">
|
||||
{attributes.check_metadata.risk}
|
||||
</p>
|
||||
</Snippet>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
|
||||
Description
|
||||
</p>
|
||||
<p className="text-gray-800 dark:text-prowler-theme-pale/90">
|
||||
{attributes.check_metadata.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="text-sm font-semibold dark:text-prowler-theme-pale">
|
||||
Remediation
|
||||
</h3>
|
||||
<div className="text-gray-800 dark:text-prowler-theme-pale/90">
|
||||
{remediation.recommendation && (
|
||||
<>
|
||||
<p className="text-sm font-semibold">Recommendation:</p>
|
||||
<p>{remediation.recommendation.text}</p>
|
||||
<Link
|
||||
target="_blank"
|
||||
href={remediation.recommendation.url}
|
||||
className="mt-2 inline-block text-sm text-blue-500 underline"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{remediation.code &&
|
||||
Object.values(remediation.code).some(Boolean) && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="mt-4 text-sm font-semibold">
|
||||
Check these links:
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
{remediation.code.cli && (
|
||||
<div>
|
||||
<p className="text-sm font-semibold">CLI Command:</p>
|
||||
<Snippet hideSymbol size="sm" className="max-w-full">
|
||||
<p className="whitespace-pre-line">
|
||||
{remediation.code.cli}
|
||||
</p>
|
||||
</Snippet>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row gap-4">
|
||||
{Object.entries(remediation.code)
|
||||
.filter(([key]) => key !== "cli")
|
||||
.map(([key, value]) =>
|
||||
value ? (
|
||||
<Link
|
||||
key={key}
|
||||
href={value}
|
||||
target="_blank"
|
||||
className="text-sm font-medium text-blue-500"
|
||||
>
|
||||
{key === "other"
|
||||
? "External doc"
|
||||
: key.charAt(0).toUpperCase() +
|
||||
key.slice(1).toLowerCase()}
|
||||
</Link>
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resources Section */}
|
||||
<div className="flex flex-col gap-4 rounded-lg p-4 shadow dark:bg-prowler-blue-400">
|
||||
<h3 className="text-lg font-bold text-gray-800 dark:text-prowler-theme-pale/90">
|
||||
Resource Details
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="col-span-2">
|
||||
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
|
||||
Resource ID
|
||||
</p>
|
||||
<Snippet size="sm" hideSymbol className="max-w-full">
|
||||
<p className="whitespace-pre-line">{resource.uid}</p>
|
||||
</Snippet>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
|
||||
Resource Name
|
||||
</p>
|
||||
<p className="text-gray-800 dark:text-prowler-theme-pale/90">
|
||||
{resource.name}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
|
||||
Region
|
||||
</p>
|
||||
<p className="text-gray-800 dark:text-prowler-theme-pale/90">
|
||||
{resource.region}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
|
||||
Resource Type
|
||||
</p>
|
||||
<p className="text-gray-800 dark:text-prowler-theme-pale/90">
|
||||
{resource.type}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
|
||||
Severity
|
||||
</p>
|
||||
<SeverityBadge severity={attributes.severity} />
|
||||
</div>
|
||||
{resource.tags &&
|
||||
Object.entries(resource.tags).map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
|
||||
Tag: {key}
|
||||
</p>
|
||||
<SnippetId
|
||||
entityId={value}
|
||||
hideSymbol
|
||||
size="sm"
|
||||
className="max-w-full"
|
||||
>
|
||||
<p className="whitespace-pre-line">{value}</p>
|
||||
</SnippetId>
|
||||
</div>
|
||||
))}
|
||||
<div className="col-span-2 grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
|
||||
Inserted At
|
||||
</p>
|
||||
<DateWithTime inline dateTime={resource.inserted_at} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold dark:text-prowler-theme-pale">
|
||||
Updated At
|
||||
</p>
|
||||
<DateWithTime inline dateTime={resource.updated_at} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
5
ui/components/findings/table/index.ts
Normal file
5
ui/components/findings/table/index.ts
Normal file
@@ -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";
|
||||
65
ui/components/findings/table/skeleton-table-findings.tsx
Normal file
65
ui/components/findings/table/skeleton-table-findings.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Card, Skeleton } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
export const SkeletonTableFindings = () => {
|
||||
return (
|
||||
<Card className="h-full w-full space-y-5 p-4" radius="sm">
|
||||
{/* Table headers */}
|
||||
<div className="hidden justify-between md:flex">
|
||||
<Skeleton className="w-1/12 rounded-lg">
|
||||
<div className="h-8 bg-default-200"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="w-2/12 rounded-lg">
|
||||
<div className="h-8 bg-default-200"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="w-2/12 rounded-lg">
|
||||
<div className="h-8 bg-default-200"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="w-2/12 rounded-lg">
|
||||
<div className="h-8 bg-default-200"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="w-2/12 rounded-lg">
|
||||
<div className="h-8 bg-default-200"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="w-1/12 rounded-lg">
|
||||
<div className="h-8 bg-default-200"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="w-1/12 rounded-lg">
|
||||
<div className="h-8 bg-default-200"></div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
|
||||
{/* Table body */}
|
||||
<div className="space-y-3">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col items-center justify-between space-x-0 md:flex-row md:space-x-4"
|
||||
>
|
||||
<Skeleton className="mb-2 w-full rounded-lg md:mb-0 md:w-1/12">
|
||||
<div className="h-12 bg-default-300"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="mb-2 w-full rounded-lg md:mb-0 md:w-2/12">
|
||||
<div className="h-12 bg-default-300"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="mb-2 hidden rounded-lg sm:flex md:mb-0 md:w-2/12">
|
||||
<div className="h-12 bg-default-300"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="mb-2 hidden rounded-lg sm:flex md:mb-0 md:w-2/12">
|
||||
<div className="h-12 bg-default-300"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="mb-2 hidden rounded-lg sm:flex md:mb-0 md:w-2/12">
|
||||
<div className="h-12 bg-default-300"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="mb-2 hidden rounded-lg sm:flex md:mb-0 md:w-1/12">
|
||||
<div className="h-12 bg-default-300"></div>
|
||||
</Skeleton>
|
||||
<Skeleton className="mb-2 hidden rounded-lg sm:flex md:mb-0 md:w-1/12">
|
||||
<div className="h-12 bg-default-300"></div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
810
ui/components/icons/Icons.tsx
Normal file
810
ui/components/icons/Icons.tsx
Normal file
@@ -0,0 +1,810 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { IconSvgProps } from "@/types";
|
||||
|
||||
export const TwitterIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M19.633 7.997c.013.175.013.349.013.523 0 5.325-4.053 11.461-11.46 11.461-2.282 0-4.402-.661-6.186-1.809.324.037.636.05.973.05a8.07 8.07 0 0 0 5.001-1.721 4.036 4.036 0 0 1-3.767-2.793c.249.037.499.062.761.062.361 0 .724-.05 1.061-.137a4.027 4.027 0 0 1-3.23-3.953v-.05c.537.299 1.16.486 1.82.511a4.022 4.022 0 0 1-1.796-3.354c0-.748.199-1.434.548-2.032a11.457 11.457 0 0 0 8.306 4.215c-.062-.3-.1-.611-.1-.923a4.026 4.026 0 0 1 4.028-4.028c1.16 0 2.207.486 2.943 1.272a7.957 7.957 0 0 0 2.556-.973 4.02 4.02 0 0 1-1.771 2.22 8.073 8.073 0 0 0 2.319-.624 8.645 8.645 0 0 1-2.019 2.083z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const GithubIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const MoonFilledIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M21.53 15.93c-.16-.27-.61-.69-1.73-.49a8.46 8.46 0 01-1.88.13 8.409 8.409 0 01-5.91-2.82 8.068 8.068 0 01-1.44-8.66c.44-1.01.13-1.54-.09-1.76s-.77-.55-1.83-.11a10.318 10.318 0 00-6.32 10.21 10.475 10.475 0 007.04 8.99 10 10 0 002.89.55c.16.01.32.02.48.02a10.5 10.5 0 008.47-4.27c.67-.93.49-1.519.32-1.79z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SunFilledIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<g fill="currentColor">
|
||||
<path d="M19 12a7 7 0 11-7-7 7 7 0 017 7z" />
|
||||
<path d="M12 22.96a.969.969 0 01-1-.96v-.08a1 1 0 012 0 1.038 1.038 0 01-1 1.04zm7.14-2.82a1.024 1.024 0 01-.71-.29l-.13-.13a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.984.984 0 01-.7.29zm-14.28 0a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a1 1 0 01-.7.29zM22 13h-.08a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zM2.08 13H2a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zm16.93-7.01a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a.984.984 0 01-.7.29zm-14.02 0a1.024 1.024 0 01-.71-.29l-.13-.14a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.97.97 0 01-.7.3zM12 3.04a.969.969 0 01-1-.96V2a1 1 0 012 0 1.038 1.038 0 01-1 1.04z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SearchIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M22 22L20 20"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ChevronDownIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
strokeWidth = 1.5,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M19.92 8.95l-6.52 6.52c-.77.77-2.03.77-2.8 0L4.08 8.95"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PlusIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path d="M6 12h12" />
|
||||
<path d="M12 18V6" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const VerticalDotsIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<g fill="currentColor">
|
||||
<path d="M12 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zM12 4c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zM12 16c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DeleteIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
height={size || height || 48}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width || 48}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258-.011.002-.071.035-.02.004-.014-.004-.071-.035q-.016-.005-.024.005l-.004.01-.017.428.005.02.01.013.104.074.015.004.012-.004.104-.074.012-.016.004-.017-.017-.427q-.004-.016-.017-.018m.265-.113-.013.002-.185.093-.01.01-.003.011.018.43.005.012.008.007.201.093q.019.005.029-.008l.004-.014-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014-.034.614q.001.018.017.024l.015-.002.201-.093.01-.008.004-.011.017-.43-.003-.012-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M14.28 2a2 2 0 0 1 1.897 1.368L16.72 5H20a1 1 0 1 1 0 2l-.003.071-.867 12.143A3 3 0 0 1 16.138 22H7.862a3 3 0 0 1-2.992-2.786L4.003 7.07 4 7a1 1 0 0 1 0-2h3.28l.543-1.632A2 2 0 0 1 9.721 2zm3.717 5H6.003l.862 12.071a1 1 0 0 0 .997.929h8.276a1 1 0 0 0 .997-.929zM10 10a1 1 0 0 1 .993.883L11 11v5a1 1 0 0 1-1.993.117L9 16v-5a1 1 0 0 1 1-1m4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1m.28-6H9.72l-.333 1h5.226z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const CheckIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
viewBox="0 0 2048 2048"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M2048 1024q0 142-36 272t-103 245t-160 207t-208 160t-245 103t-272 37q-142 0-272-36t-245-103t-207-160t-160-208t-103-245t-37-272q0-141 36-272t103-245t160-207t208-160T752 37t272-37q141 0 272 36t245 103t207 160t160 208t103 245t37 272m-1024 896q123 0 237-32t214-90t182-141t140-181t91-214t32-238q0-123-32-237t-90-214t-141-182t-181-140t-214-91t-238-32q-124 0-238 32t-213 90t-182 141t-140 181t-91 214t-32 238q0 124 32 238t90 213t141 182t181 140t214 91t238 32m0-512q55 0 107-15t98-45t81-69t61-91l116 56q-32 67-80 121t-109 92t-130 58t-144 21q-110 0-210-45t-174-128v173H512v-384h384v128H738q54 60 129 94t157 34m384-723V512h128v384h-384V768h158q-54-60-129-94t-157-34q-55 0-107 15t-98 45t-81 69t-61 91l-116-56q32-67 80-121t109-92t130-58t144-21q110 0 210 45t174 128"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CrossIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16.066 8.995a.75.75 0 1 0-1.06-1.061L12 10.939L8.995 7.934a.75.75 0 1 0-1.06 1.06L10.938 12l-3.005 3.005a.75.75 0 0 0 1.06 1.06L12 13.06l3.005 3.006a.75.75 0 0 0 1.06-1.06L13.062 12z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PassIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
viewBox="0 0 16 16"
|
||||
{...props}
|
||||
>
|
||||
<g fill="currentColor">
|
||||
<path d="M6.27 10.87h.71l4.56-4.56l-.71-.71l-4.2 4.21l-1.92-1.92L4 8.6z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8.6 1c1.6.1 3.1.9 4.2 2c1.3 1.4 2 3.1 2 5.1c0 1.6-.6 3.1-1.6 4.4c-1 1.2-2.4 2.1-4 2.4s-3.2.1-4.6-.7s-2.5-2-3.1-3.5S.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1m.5 12.9c1.3-.3 2.5-1 3.4-2.1c.8-1.1 1.3-2.4 1.2-3.8c0-1.6-.6-3.2-1.7-4.3c-1-1-2.2-1.6-3.6-1.7c-1.3-.1-2.7.2-3.8 1S2.7 4.9 2.3 6.3c-.4 1.3-.4 2.7.2 4q.9 1.95 2.7 3c1.2.7 2.6.9 3.9.6"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const RocketIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m5.65 10.025l1.95.825q.35-.7.725-1.35t.825-1.3l-1.4-.275zM9.2 12.1l2.85 2.825q1.05-.4 2.25-1.225t2.25-1.875q1.75-1.75 2.738-3.887T20.15 4q-1.8-.125-3.95.863T12.3 7.6q-1.05 1.05-1.875 2.25T9.2 12.1m4.45-1.625q-.575-.575-.575-1.412t.575-1.413t1.425-.575t1.425.575t.575 1.413t-.575 1.412t-1.425.575t-1.425-.575m.475 8.025l2.1-2.1l-.275-1.4q-.65.45-1.3.812t-1.35.713zM21.95 2.175q.475 3.025-.587 5.888T17.7 13.525L18.2 16q.1.5-.05.975t-.5.825l-4.2 4.2l-2.1-4.925L7.075 12.8L2.15 10.7l4.175-4.2q.35-.35.838-.5t.987-.05l2.475.5q2.6-2.6 5.45-3.675t5.875-.6m-18.025 13.8q.875-.875 2.138-.887t2.137.862t.863 2.138t-.888 2.137q-.625.625-2.087 1.075t-4.038.8q.35-2.575.8-4.038t1.075-2.087m1.425 1.4q-.25.25-.5.913t-.35 1.337q.675-.1 1.338-.337t.912-.488q.3-.3.325-.725T6.8 17.35t-.725-.288t-.725.313"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const AlertIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m13.299 3.148l8.634 14.954a1.5 1.5 0 0 1-1.299 2.25H3.366a1.5 1.5 0 0 1-1.299-2.25l8.634-14.954c.577-1 2.02-1 2.598 0M12 4.898L4.232 18.352h15.536zM12 15a1 1 0 1 1 0 2a1 1 0 0 1 0-2m0-7a1 1 0 0 1 1 1v4a1 1 0 1 1-2 0V9a1 1 0 0 1 1-1"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const NotificationIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={size || height || 24}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width || 24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M18.707 8.796c0 1.256.332 1.997 1.063 2.85.553.628.73 1.435.73 2.31 0 .874-.287 1.704-.863 2.378a4.537 4.537 0 01-2.9 1.413c-1.571.134-3.143.247-4.736.247-1.595 0-3.166-.068-4.737-.247a4.532 4.532 0 01-2.9-1.413 3.616 3.616 0 01-.864-2.378c0-.875.178-1.682.73-2.31.754-.854 1.064-1.594 1.064-2.85V8.37c0-1.682.42-2.781 1.283-3.858C7.861 2.942 9.919 2 11.956 2h.09c2.08 0 4.204.987 5.466 2.625.82 1.054 1.195 2.108 1.195 3.745v.426zM9.074 20.061c0-.504.462-.734.89-.833.5-.106 3.545-.106 4.045 0 .428.099.89.33.89.833-.025.48-.306.904-.695 1.174a3.635 3.635 0 01-1.713.731 3.795 3.795 0 01-1.008 0 3.618 3.618 0 01-1.714-.732c-.39-.269-.67-.694-.695-1.173z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IdIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
height={size || height || 24}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width || 24}
|
||||
{...props}
|
||||
>
|
||||
<path d="M18 4v16H6V8.8L10.8 4zm0-2h-8L4 8v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2M9.5 19h-2v-2h2zm7 0h-2v-2h2zm-7-4h-2v-4h2zm3.5 4h-2v-4h2zm0-6h-2v-2h2zm3.5 2h-2v-4h2z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DoneIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
width={size || width || 24}
|
||||
height={size || height || 24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path d="m2.394 13.742 4.743 3.62 7.616-8.704-1.506-1.316-6.384 7.296-3.257-2.486zm19.359-5.084-1.506-1.316-6.369 7.279-.753-.602-1.25 1.562 2.247 1.798z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const CopyIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height={size || height || 20}
|
||||
shapeRendering="geometricPrecision"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width || 20}
|
||||
{...props}
|
||||
>
|
||||
<path d="M6 17C4.89543 17 4 16.1046 4 15V5C4 3.89543 4.89543 3 6 3H13C13.7403 3 14.3866 3.4022 14.7324 4M11 21H18C19.1046 21 20 20.1046 20 19V9C20 7.89543 19.1046 7 18 7H11C9.89543 7 9 7.89543 9 9V19C9 20.1046 9.89543 21 11 21Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const FlowIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
height={size || height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
width={size || width || 20}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16.4 4a2.4 2.4 0 1 0-4.8 0c0 .961.568 1.784 1.384 2.167c-.082 1.584-1.27 2.122-3.335 2.896c-.87.327-1.829.689-2.649 1.234V6.176A2.396 2.396 0 0 0 6 1.6a2.397 2.397 0 0 0-1 4.576v7.649A2.39 2.39 0 0 0 3.6 16a2.4 2.4 0 1 0 4.8 0c0-.961-.568-1.784-1.384-2.167c.082-1.583 1.271-2.122 3.335-2.896c2.03-.762 4.541-1.711 4.64-4.756A2.4 2.4 0 0 0 16.4 4M6 2.615a1.384 1.384 0 1 1 0 2.768a1.384 1.384 0 0 1 0-2.768m0 14.77a1.385 1.385 0 1 1 0-2.77a1.385 1.385 0 0 1 0 2.77m8-12a1.385 1.385 0 1 1 0-2.77a1.385 1.385 0 0 1 0 2.77"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectionIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
height={size || height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
width={size || width || 20}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M18 14.824V12.5A3.5 3.5 0 0 0 14.5 9h-2A1.5 1.5 0 0 1 11 7.5V5.176A2.4 2.4 0 0 0 12.4 3a2.4 2.4 0 1 0-4.8 0c0 .967.576 1.796 1.4 2.176V7.5A1.5 1.5 0 0 1 7.5 9h-2A3.5 3.5 0 0 0 2 12.5v2.324A2.396 2.396 0 0 0 3 19.4a2.397 2.397 0 0 0 1-4.576V12.5A1.5 1.5 0 0 1 5.5 11h2c.539 0 1.044-.132 1.5-.35v4.174a2.396 2.396 0 0 0 1 4.576a2.397 2.397 0 0 0 1-4.576V10.65c.456.218.961.35 1.5.35h2a1.5 1.5 0 0 1 1.5 1.5v2.324A2.4 2.4 0 0 0 14.6 17a2.4 2.4 0 1 0 4.8 0c0-.967-.575-1.796-1.4-2.176M10 1.615a1.384 1.384 0 1 1 0 2.768a1.384 1.384 0 0 1 0-2.768m-7 16.77a1.385 1.385 0 1 1 0-2.77a1.385 1.385 0 0 1 0 2.77m7 0a1.385 1.385 0 1 1 0-2.77a1.385 1.385 0 0 1 0 2.77m7 0a1.385 1.385 0 1 1 0-2.77a1.385 1.385 0 0 1 0 2.77"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectionTrue: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
height={size || height || 24}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width || 24}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12 20h.012M8.25 17c2-2 5.5-2 7.5 0m2.75-3c-3.768-3.333-9-3.333-13 0M2 11c3.158-2.667 6.579-4 10-4m3 .5s1 0 2 2c0 0 2.477-3.9 5-5.5"
|
||||
color="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ConnectionFalse: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
height={size || height || 24}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width || 24}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12 18h.012M8.25 15c2-2 5.5-2 7.5 0m2.75-3a11 11 0 0 0-.231-.199M5.5 12c2.564-2.136 5.634-2.904 8.5-2.301M2 9c3.466-2.927 7.248-4.247 11-3.962M22 5l-6 6m6 0-6-6"
|
||||
color="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ConnectionPending: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.05"
|
||||
height={size || height || 24}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width || 24}
|
||||
{...props}
|
||||
>
|
||||
<g fill="none" stroke="currentColor" strokeWidth="1.05">
|
||||
<circle cx="12" cy="18" r="2" />
|
||||
<path strokeOpacity=".2" d="M7.757 13.757a6 6 0 0 1 8.486 0" />
|
||||
<path
|
||||
strokeOpacity=".2"
|
||||
d="M4.929 10.93c3.905-3.905 10.237-3.905 14.142 0"
|
||||
opacity=".8"
|
||||
/>
|
||||
<path
|
||||
strokeOpacity=".2"
|
||||
d="M2.101 8.1c5.467-5.468 14.331-5.468 19.798 0"
|
||||
opacity=".8"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SuccessIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
width={size || width || 24}
|
||||
height={size || height || 24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.49 2 2 6.49 2 12C2 17.51 6.49 22 12 22C17.51 22 22 17.51 22 12C22 6.49 17.51 2 12 2ZM16.78 9.7L11.11 15.37C10.97 15.51 10.78 15.59 10.58 15.59C10.38 15.59 10.19 15.51 10.05 15.37L7.22 12.54C6.93 12.25 6.93 11.77 7.22 11.48C7.51 11.19 7.99 11.19 8.28 11.48L10.58 13.78L15.72 8.64C16.01 8.35 16.49 8.35 16.78 8.64C17.07 8.93 17.07 9.4 16.78 9.7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ArrowUpIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
height={size || height || "1em"}
|
||||
viewBox="0 0 12 12"
|
||||
width={size || width || "1em"}
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M3 7.5L6 4.5L9 7.5"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ArrowDownIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
height={size || height || "1em"}
|
||||
viewBox="0 0 12 12"
|
||||
width={size || width || "1em"}
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M3 4.5L6 7.5L9 4.5"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChevronsLeftRightIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
height={size || height || 24}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width || 24}
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-chevrons-left-right ml-2 h-4 w-4 rotate-90"
|
||||
{...props}
|
||||
>
|
||||
<path d="m9 7-5 5 5 5M15 7l5 5-5 5" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const PlusCircleIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
height={size || height || 15}
|
||||
viewBox="0 0 15 15"
|
||||
width={size || width || 15}
|
||||
className="mr-2 size-4"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M7.5.877a6.623 6.623 0 1 0 0 13.246A6.623 6.623 0 0 0 7.5.877ZM1.827 7.5a5.673 5.673 0 1 1 11.346 0 5.673 5.673 0 0 1-11.346 0ZM7.5 4a.5.5 0 0 1 .5.5V7h2.5a.5.5 0 1 1 0 1H8v2.5a.5.5 0 0 1-1 0V8H4.5a.5.5 0 0 1 0-1H7V4.5a.5.5 0 0 1 .5-.5Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomFilterIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
height={size || height || 16}
|
||||
width={size || width || 16}
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<g fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M9.5 14a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm5-10a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z" />
|
||||
<path strokeLinecap="round" d="M15 16.959h7m-13-10H2m0 10h2m18-10h-2" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const SaveIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
height={size || height || 48}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width || 48}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="m20.71 9.29l-6-6a1 1 0 0 0-.32-.21A1.1 1.1 0 0 0 14 3H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3v-8a1 1 0 0 0-.29-.71M9 5h4v2H9Zm6 14H9v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1Zm4-1a1 1 0 0 1-1 1h-1v-3a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3v3H6a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1v3a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V6.41l4 4Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
height={size || height || 20}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width || 20}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10m.75-13a.75.75 0 0 0-1.5 0v2.25H9a.75.75 0 0 0 0 1.5h2.25V15a.75.75 0 0 0 1.5 0v-2.25H15a.75.75 0 0 0 0-1.5h-2.25z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScheduleIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size || width || 24}
|
||||
height={size || height || 24}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5" />
|
||||
<path d="M16 2v4M8 2v4M3 10h5" />
|
||||
<path d="M17.5 17.5L16 16.3V14" />
|
||||
<circle cx="16" cy="16" r="6" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const InfoIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 16v-4M12 8h.01" />
|
||||
</svg>
|
||||
);
|
||||
63
ui/components/icons/compliance/IconCompliance.tsx
Normal file
63
ui/components/icons/compliance/IconCompliance.tsx
Normal file
@@ -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;
|
||||
}
|
||||
};
|
||||
38
ui/components/icons/compliance/aws.svg
Normal file
38
ui/components/icons/compliance/aws.svg
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 304 182" style="enable-background:new 0 0 304 182;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#252F3E;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FF9900;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M86.4,66.4c0,3.7,0.4,6.7,1.1,8.9c0.8,2.2,1.8,4.6,3.2,7.2c0.5,0.8,0.7,1.6,0.7,2.3c0,1-0.6,2-1.9,3l-6.3,4.2
|
||||
c-0.9,0.6-1.8,0.9-2.6,0.9c-1,0-2-0.5-3-1.4C76.2,90,75,88.4,74,86.8c-1-1.7-2-3.6-3.1-5.9c-7.8,9.2-17.6,13.8-29.4,13.8
|
||||
c-8.4,0-15.1-2.4-20-7.2c-4.9-4.8-7.4-11.2-7.4-19.2c0-8.5,3-15.4,9.1-20.6c6.1-5.2,14.2-7.8,24.5-7.8c3.4,0,6.9,0.3,10.6,0.8
|
||||
c3.7,0.5,7.5,1.3,11.5,2.2v-7.3c0-7.6-1.6-12.9-4.7-16c-3.2-3.1-8.6-4.6-16.3-4.6c-3.5,0-7.1,0.4-10.8,1.3c-3.7,0.9-7.3,2-10.8,3.4
|
||||
c-1.6,0.7-2.8,1.1-3.5,1.3c-0.7,0.2-1.2,0.3-1.6,0.3c-1.4,0-2.1-1-2.1-3.1v-4.9c0-1.6,0.2-2.8,0.7-3.5c0.5-0.7,1.4-1.4,2.8-2.1
|
||||
c3.5-1.8,7.7-3.3,12.6-4.5c4.9-1.3,10.1-1.9,15.6-1.9c11.9,0,20.6,2.7,26.2,8.1c5.5,5.4,8.3,13.6,8.3,24.6V66.4z M45.8,81.6
|
||||
c3.3,0,6.7-0.6,10.3-1.8c3.6-1.2,6.8-3.4,9.5-6.4c1.6-1.9,2.8-4,3.4-6.4c0.6-2.4,1-5.3,1-8.7v-4.2c-2.9-0.7-6-1.3-9.2-1.7
|
||||
c-3.2-0.4-6.3-0.6-9.4-0.6c-6.7,0-11.6,1.3-14.9,4c-3.3,2.7-4.9,6.5-4.9,11.5c0,4.7,1.2,8.2,3.7,10.6
|
||||
C37.7,80.4,41.2,81.6,45.8,81.6z M126.1,92.4c-1.8,0-3-0.3-3.8-1c-0.8-0.6-1.5-2-2.1-3.9L96.7,10.2c-0.6-2-0.9-3.3-0.9-4
|
||||
c0-1.6,0.8-2.5,2.4-2.5h9.8c1.9,0,3.2,0.3,3.9,1c0.8,0.6,1.4,2,2,3.9l16.8,66.2l15.6-66.2c0.5-2,1.1-3.3,1.9-3.9c0.8-0.6,2.2-1,4-1
|
||||
h8c1.9,0,3.2,0.3,4,1c0.8,0.6,1.5,2,1.9,3.9l15.8,67l17.3-67c0.6-2,1.3-3.3,2-3.9c0.8-0.6,2.1-1,3.9-1h9.3c1.6,0,2.5,0.8,2.5,2.5
|
||||
c0,0.5-0.1,1-0.2,1.6c-0.1,0.6-0.3,1.4-0.7,2.5l-24.1,77.3c-0.6,2-1.3,3.3-2.1,3.9c-0.8,0.6-2.1,1-3.8,1h-8.6c-1.9,0-3.2-0.3-4-1
|
||||
c-0.8-0.7-1.5-2-1.9-4L156,23l-15.4,64.4c-0.5,2-1.1,3.3-1.9,4c-0.8,0.7-2.2,1-4,1H126.1z M254.6,95.1c-5.2,0-10.4-0.6-15.4-1.8
|
||||
c-5-1.2-8.9-2.5-11.5-4c-1.6-0.9-2.7-1.9-3.1-2.8c-0.4-0.9-0.6-1.9-0.6-2.8v-5.1c0-2.1,0.8-3.1,2.3-3.1c0.6,0,1.2,0.1,1.8,0.3
|
||||
c0.6,0.2,1.5,0.6,2.5,1c3.4,1.5,7.1,2.7,11,3.5c4,0.8,7.9,1.2,11.9,1.2c6.3,0,11.2-1.1,14.6-3.3c3.4-2.2,5.2-5.4,5.2-9.5
|
||||
c0-2.8-0.9-5.1-2.7-7c-1.8-1.9-5.2-3.6-10.1-5.2L246,52c-7.3-2.3-12.7-5.7-16-10.2c-3.3-4.4-5-9.3-5-14.5c0-4.2,0.9-7.9,2.7-11.1
|
||||
c1.8-3.2,4.2-6,7.2-8.2c3-2.3,6.4-4,10.4-5.2c4-1.2,8.2-1.7,12.6-1.7c2.2,0,4.5,0.1,6.7,0.4c2.3,0.3,4.4,0.7,6.5,1.1
|
||||
c2,0.5,3.9,1,5.7,1.6c1.8,0.6,3.2,1.2,4.2,1.8c1.4,0.8,2.4,1.6,3,2.5c0.6,0.8,0.9,1.9,0.9,3.3v4.7c0,2.1-0.8,3.2-2.3,3.2
|
||||
c-0.8,0-2.1-0.4-3.8-1.2c-5.7-2.6-12.1-3.9-19.2-3.9c-5.7,0-10.2,0.9-13.3,2.8c-3.1,1.9-4.7,4.8-4.7,8.9c0,2.8,1,5.2,3,7.1
|
||||
c2,1.9,5.7,3.8,11,5.5l14.2,4.5c7.2,2.3,12.4,5.5,15.5,9.6c3.1,4.1,4.6,8.8,4.6,14c0,4.3-0.9,8.2-2.6,11.6
|
||||
c-1.8,3.4-4.2,6.4-7.3,8.8c-3.1,2.5-6.8,4.3-11.1,5.6C264.4,94.4,259.7,95.1,254.6,95.1z"/>
|
||||
<g>
|
||||
<path class="st1" d="M273.5,143.7c-32.9,24.3-80.7,37.2-121.8,37.2c-57.6,0-109.5-21.3-148.7-56.7c-3.1-2.8-0.3-6.6,3.4-4.4
|
||||
c42.4,24.6,94.7,39.5,148.8,39.5c36.5,0,76.6-7.6,113.5-23.2C274.2,133.6,278.9,139.7,273.5,143.7z"/>
|
||||
<path class="st1" d="M287.2,128.1c-4.2-5.4-27.8-2.6-38.5-1.3c-3.2,0.4-3.7-2.4-0.8-4.5c18.8-13.2,49.7-9.4,53.3-5
|
||||
c3.6,4.5-1,35.4-18.6,50.2c-2.7,2.3-5.3,1.1-4.1-1.9C282.5,155.7,291.4,133.4,287.2,128.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
9
ui/components/icons/compliance/cis.svg
Normal file
9
ui/components/icons/compliance/cis.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 128 KiB |
2
ui/components/icons/compliance/cisa.svg
Normal file
2
ui/components/icons/compliance/cisa.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 20 KiB |
BIN
ui/components/icons/compliance/ens.png
Normal file
BIN
ui/components/icons/compliance/ens.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
9
ui/components/icons/compliance/fedramp.svg
Normal file
9
ui/components/icons/compliance/fedramp.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 47 KiB |
9
ui/components/icons/compliance/ffiec.svg
Normal file
9
ui/components/icons/compliance/ffiec.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 24 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user