mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-11-08 20:33:41 +00:00
Merge branch 'master' into PROWLER-179-aws-add-an-connect-the-provider
This commit is contained in:
1364 files changed
+69177
-3010
No files matched your search
+854
@@ -0,0 +1,854 @@
|
||||
# Prowler UI Agent Guide
|
||||
|
||||
**Complete guide for AI agents and developers working on the Prowler UI Next.js application.**
|
||||
|
||||
## Mission & Scope
|
||||
|
||||
- Ship small, high-impact UI changes with minimal risk
|
||||
- Align to current patterns: App Router, Server Components first, consistent styling, strict types
|
||||
- Avoid broad refactors, library swaps, or reorganization unless requested
|
||||
- Focus on safe, incremental frontend changes aligned with existing architecture
|
||||
|
||||
---
|
||||
|
||||
## Critical Architecture Rules (Non-Negotiable)
|
||||
|
||||
### 1. TypeScript Type Patterns (Required)
|
||||
|
||||
When defining union types for options, ALWAYS create a const object first, then extract the type:
|
||||
|
||||
**❌ DON'T:**
|
||||
|
||||
```typescript
|
||||
type SortOption = "high-low" | "low-high" | "alphabetical";
|
||||
```
|
||||
|
||||
**✅ DO:**
|
||||
|
||||
```typescript
|
||||
const SORT_OPTIONS = {
|
||||
HIGH_LOW: "high-low",
|
||||
LOW_HIGH: "low-high",
|
||||
ALPHABETICAL: "alphabetical",
|
||||
} as const;
|
||||
|
||||
type SortOption = (typeof SORT_OPTIONS)[keyof typeof SORT_OPTIONS];
|
||||
```
|
||||
|
||||
### 2. Tailwind 4 Theme Variables
|
||||
|
||||
This project uses Tailwind 4 with @theme variables. **Tailwind is mainly semantic** - prioritize using Tailwind's naming system whenever possible.
|
||||
|
||||
#### In Template/JSX (className)
|
||||
|
||||
- ✅ Use Tailwind utility classes: `bg-card-bg`, `text-white`, `text-slate-400`, `border-slate-700`
|
||||
- ✅ Use arbitrary values with classes: `h-3`, `w-3`, `min-w-[200px]`, `bg-slate-700/50`
|
||||
- ✅ Use Tailwind for conditional styles: `className={isActive ? "bg-blue-500" : "bg-gray-500"}`
|
||||
- ✅ Use style props only for truly dynamic values: `style={{ width: \`\${percentage}%\` }}`
|
||||
- ❌ NEVER use `var()` in className
|
||||
- ❌ NEVER use hex colors in className
|
||||
|
||||
#### Constants with var() (Only for library props that don't accept className)
|
||||
|
||||
- ✅ Use CHART_COLORS constants: `stroke={CHART_COLORS.gridLine}`, `tick={{ fill: CHART_COLORS.textSecondary }}`
|
||||
- These props don't accept className, so we use constants that internally reference `var()`
|
||||
- This is the **only** valid use case for `var()` - when the library doesn't support className
|
||||
|
||||
#### Examples
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Template with Tailwind classes
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3">
|
||||
<p className="text-sm font-semibold text-white">{title}</p>
|
||||
<Bell size={14} className="text-slate-400" />
|
||||
</div>
|
||||
|
||||
// ✅ GOOD - Conditional Tailwind classes
|
||||
<button className={isActive ? "bg-blue-500" : "bg-gray-500"}>
|
||||
Click me
|
||||
</button>
|
||||
|
||||
// ✅ GOOD - Recharts library props with CHART_COLORS (var() only here)
|
||||
<XAxis tick={{ fill: CHART_COLORS.textSecondary, fontSize: 12 }} />
|
||||
<CartesianGrid stroke={CHART_COLORS.gridLine} />
|
||||
|
||||
// ✅ GOOD - Truly dynamic values (not available in Tailwind)
|
||||
<div style={{ width: `${percentage}%`, opacity: isFaded ? 0.5 : 1 }} />
|
||||
|
||||
// ❌ BAD - var() in className
|
||||
<div className="bg-[var(--color-card-bg)]" /> // Don't do this!
|
||||
|
||||
// ❌ BAD - Hex colors in className
|
||||
<p className="text-[#ffffff]" /> // Use text-white instead
|
||||
|
||||
// ❌ BAD - Using var() for colors when Tailwind classes exist
|
||||
const PROVIDER_COLORS = {
|
||||
AWS: "var(--color-orange)", // Use Tailwind classes instead!
|
||||
};
|
||||
|
||||
// ❌ BAD - Using style when className is available
|
||||
<div style={{ backgroundColor: "blue" }} /> // Use className="bg-blue-500" instead
|
||||
```
|
||||
|
||||
### 3. The `cn()` Utility Function
|
||||
|
||||
#### What is `cn()`?
|
||||
|
||||
The `cn()` function is a utility that combines `clsx` and `tailwind-merge`:
|
||||
|
||||
```typescript
|
||||
import { clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
```
|
||||
|
||||
**Components:**
|
||||
|
||||
- **`clsx`**: Constructs conditional className strings (handles booleans, arrays, objects)
|
||||
- **`twMerge`**: Intelligently merges Tailwind classes, resolving conflicts (e.g., `p-4` + `p-2` → `p-2`)
|
||||
|
||||
#### When to Use `cn()`
|
||||
|
||||
Use `cn()` **ONLY** when you have:
|
||||
|
||||
##### 1. Conditional Classes
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Conditional logic
|
||||
<div className={cn("h-3 w-3", isCircle ? "rounded-full" : "rounded-sm")} />
|
||||
|
||||
// ✅ GOOD - Boolean conditionals
|
||||
<button className={cn("btn", isActive && "btn-active", isDisabled && "opacity-50")} />
|
||||
```
|
||||
|
||||
##### 2. Merging Props with Conflicting Classes
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Component accepting className prop
|
||||
interface ButtonProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Button({ className }: ButtonProps) {
|
||||
return <button className={cn("bg-blue-500 px-4 py-2", className)} />;
|
||||
}
|
||||
|
||||
// Usage: <Button className="px-6" /> → Results in "px-6 py-2 bg-blue-500"
|
||||
```
|
||||
|
||||
##### 3. Dynamic String Interpolation
|
||||
|
||||
```tsx
|
||||
// ✅ GOOD - Dynamic values that need proper merging
|
||||
<span className={cn(`text-${size}`, "font-semibold text-white")} />
|
||||
```
|
||||
|
||||
#### When NOT to Use `cn()`
|
||||
|
||||
**DO NOT** use `cn()` for static classes without conditional logic:
|
||||
|
||||
```tsx
|
||||
// ❌ BAD - Unnecessary, no conditional logic
|
||||
<div className={cn("rounded-lg border border-slate-700 bg-slate-800 p-3")} />
|
||||
|
||||
// ✅ GOOD - Just use className directly
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3" />
|
||||
|
||||
// ❌ BAD - No conflicts or conditionals
|
||||
<div className={cn("flex items-center gap-2")} />
|
||||
|
||||
// ✅ GOOD - Static classes don't need cn()
|
||||
<div className="flex items-center gap-2" />
|
||||
```
|
||||
|
||||
#### Real-World Examples
|
||||
|
||||
```tsx
|
||||
// ❌ BAD - Overuse of cn()
|
||||
function Tooltip({ active, payload }: any) {
|
||||
return (
|
||||
<div className={cn("rounded-lg border border-slate-700")}>
|
||||
<div className={cn("flex items-center gap-2")}>
|
||||
<div className={cn("h-3 w-3 rounded-sm")} />
|
||||
<span className={cn("text-sm font-semibold text-white")}>
|
||||
{payload.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ GOOD - Only use cn() where needed
|
||||
function Tooltip({ active, payload, shape }: any) {
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"h-3 w-3",
|
||||
shape === "circle" ? "rounded-full" : "rounded-sm",
|
||||
)}
|
||||
/>
|
||||
<span className="text-sm font-semibold text-white">{payload.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Key Takeaway
|
||||
|
||||
**`cn()` is a tool for conditional logic and conflict resolution, NOT a wrapper for every className.**
|
||||
|
||||
Use it purposefully where it adds value. Don't use it out of habit.
|
||||
|
||||
**Reference:** [The Story Behind Tailwind's cn() Function](https://tigerabrodi.blog/the-story-behind-tailwinds-cn-function)
|
||||
|
||||
### 4. React 19 with Compiler
|
||||
|
||||
This project uses React 19 with the React Compiler enabled. This means:
|
||||
|
||||
- **DO NOT use `useMemo`** - React Compiler handles memoization automatically
|
||||
- **DO NOT use `useCallback`** - React Compiler optimizes callbacks automatically
|
||||
- Only use these hooks if you have a specific, documented reason that the compiler cannot handle
|
||||
|
||||
### 5. Next.js 15 Architecture Principles
|
||||
|
||||
#### 1. App Router Architecture First
|
||||
|
||||
- **ALL routes MUST use App Router** - never use Pages Router for new projects
|
||||
- Leverage Server Components by default, Client Components only when necessary
|
||||
- Use proper file conventions: `page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`
|
||||
- Implement route groups `(group-name)` for organization without affecting URL structure
|
||||
- Use private folders `_folder` to opt out of routing system
|
||||
|
||||
#### 2. Server-First Architecture
|
||||
|
||||
- **Server Components by default** - add `"use client"` only when required
|
||||
- Optimize data fetching at the server level
|
||||
- Implement streaming with `loading.tsx` and Suspense boundaries
|
||||
- Use Server Actions for form handling and mutations
|
||||
- Leverage static generation and ISR for performance
|
||||
- Use DAL (Data Access Layer) patterns to separate data logic
|
||||
- To prevent accidental usage in Client Components, you can use the server-only package, this is a MUST for Server Actions and recommended for all server-only code
|
||||
|
||||
#### 3. The Scope Rule - Your Unbreakable Law
|
||||
|
||||
**"Scope determines structure"**
|
||||
|
||||
- Code used by 2+ features → MUST go in global/shared directories
|
||||
- Code used by 1 feature → MUST stay local in that feature
|
||||
- NO EXCEPTIONS - This rule is absolute and non-negotiable
|
||||
|
||||
#### 4. Screaming Architecture
|
||||
|
||||
Your structures must IMMEDIATELY communicate what the application does:
|
||||
|
||||
- Feature names must describe business functionality, not technical implementation
|
||||
- Directory structure should tell the story of what the app does at first glance
|
||||
- Route structure should mirror business logic, not technical concerns
|
||||
|
||||
#### 5. Component Placement Decision Framework
|
||||
|
||||
When analyzing where to place a component, you MUST follow this process:
|
||||
|
||||
1. **Identify component type**: Server Component, Client Component, or hybrid
|
||||
2. **Count usage**: Identify exactly how many features/routes use the component
|
||||
3. **Apply the Scope Rule**:
|
||||
- Used by 1 feature → Local placement within that feature
|
||||
- Used by 2+ features → Global/shared directory
|
||||
4. **Consider performance**: Optimize bundle splitting and server-side rendering
|
||||
5. **Document the decision**: Always explain WHY the placement was chosen
|
||||
|
||||
### 6. File Organization Patterns
|
||||
|
||||
#### Co-location Principles
|
||||
|
||||
Follow these patterns for organizing related files:
|
||||
|
||||
**For Server Actions:**
|
||||
|
||||
```
|
||||
ui/actions/
|
||||
└── feature-name/
|
||||
├── feature-name.ts # Server actions
|
||||
├── models.ts # Domain models/types
|
||||
└── feature-name.adapter.ts # Data transformations (if needed)
|
||||
```
|
||||
|
||||
**For Components:**
|
||||
|
||||
```
|
||||
ui/components/
|
||||
└── feature-name/
|
||||
├── feature-component.tsx # Main component
|
||||
├── feature-client.tsx # Client-specific logic
|
||||
└── feature-name/ # Utilities folder
|
||||
├── types.ts
|
||||
├── utils.ts
|
||||
├── constants.ts
|
||||
└── hooks.ts
|
||||
```
|
||||
|
||||
**Key Rules:**
|
||||
|
||||
- Models used by only one feature → Keep local in that feature's directory
|
||||
- Utilities used by only one feature → Keep in feature's utils folder
|
||||
- Types only used within a feature → Keep in feature's types file
|
||||
- Only move to global/shared when used by 2+ features
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
The Prowler UI is a Next.js 15 application providing a modern web interface for the Prowler security platform. It features a comprehensive dashboard for managing cloud security scans, compliance frameworks, and findings across multiple cloud providers.
|
||||
|
||||
## Tech Stack (Updated January 2025)
|
||||
|
||||
- **Framework**: Next.js 15.5.3 with App Router
|
||||
- **Runtime**: React 19.1.1 (with Compiler enabled)
|
||||
- **Language**: TypeScript 5.5.4
|
||||
- **Styling**: Tailwind CSS 4.1.13 + **shadcn/ui** (new components) / HeroUI 2.8.4 (legacy)
|
||||
- **State Management**: Zustand 5.0.8
|
||||
- **Authentication**: NextAuth.js 5.0.0-beta.29
|
||||
- **Forms/Validation**: React Hook Form 7.62.0 + Zod 4.1.11
|
||||
- **AI/Chat**: AI SDK 5.0.59 + @ai-sdk/react 2.0.59
|
||||
- **AI Backend**: LangChain @langchain/core 0.3.77 + @ai-sdk/langchain 1.0.59
|
||||
- **Charts**: Recharts 2.15.4
|
||||
- **Testing**: Playwright 1.53.2
|
||||
- **Formatter**: Prettier 3.6.2
|
||||
|
||||
## Commands
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
npm install # Install dependencies
|
||||
npm run dev # Start development server (localhost:3000)
|
||||
npm run build # Build for production
|
||||
npm start # Start production server
|
||||
npm run start:standalone # Start standalone server
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
npm run typecheck # TypeScript type checking
|
||||
npm run lint:check # ESLint checking
|
||||
npm run lint:fix # Fix ESLint issues
|
||||
npm run format:check # Prettier format checking
|
||||
npm run format:write # Format code with Prettier
|
||||
npm run healthcheck # Run typecheck + lint together
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
npm run test:e2e # Run Playwright tests
|
||||
npm run test:e2e:ui # Run tests with UI
|
||||
npm run test:e2e:debug # Debug tests
|
||||
npm run test:e2e:headed # Run tests in headed mode
|
||||
npm run test:e2e:report # Show test report
|
||||
npm run test:e2e:install # Install Playwright browsers
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
ui/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── (auth)/ # Authentication pages (sign-in, sign-up)
|
||||
│ ├── (prowler)/ # Main application pages
|
||||
│ │ ├── compliance/ # Compliance frameworks & reports
|
||||
│ │ ├── findings/ # Security findings & vulnerabilities
|
||||
│ │ ├── integrations/ # S3, Security Hub integrations
|
||||
│ │ ├── lighthouse/ # AI-powered security assistant
|
||||
│ │ ├── providers/ # Cloud provider management
|
||||
│ │ ├── scans/ # Security scan management
|
||||
│ │ └── services/ # Cloud services overview
|
||||
│ └── api/ # API routes & server actions
|
||||
├── components/ # Reusable UI components
|
||||
│ ├── shadcn/ # shadcn/ui components (NEW)
|
||||
│ │ ├── card.tsx # shadcn Card component
|
||||
│ │ ├── resource-stats-card/ # Custom ResourceStatsCard built on shadcn
|
||||
│ │ │ ├── resource-stats-card.tsx
|
||||
│ │ │ ├── resource-stats-card.example.tsx
|
||||
│ │ │ └── index.ts
|
||||
│ │ ├── index.ts # Barrel exports
|
||||
│ │ └── README.md
|
||||
│ ├── ui/ # Base UI components (buttons, forms, etc.)
|
||||
│ ├── compliance/ # Compliance-specific components
|
||||
│ ├── findings/ # Findings table & filters
|
||||
│ ├── providers/ # Provider management UI
|
||||
│ ├── scans/ # Scan management UI
|
||||
│ └── integrations/ # Integration configuration
|
||||
├── actions/ # Server actions (data fetching/mutations)
|
||||
├── lib/ # Utility functions & configurations
|
||||
├── types/ # TypeScript type definitions
|
||||
├── hooks/ # Custom React hooks
|
||||
├── store/ # Zustand state management
|
||||
└── styles/ # Global CSS & Tailwind config
|
||||
```
|
||||
|
||||
## shadcn/ui Components
|
||||
|
||||
### Directory Structure
|
||||
|
||||
All shadcn/ui based components are located in `components/shadcn/`:
|
||||
|
||||
```
|
||||
shadcn/
|
||||
├── card.tsx # shadcn Card component
|
||||
├── resource-stats-card/ # Custom ResourceStatsCard built on shadcn
|
||||
│ ├── resource-stats-card.tsx
|
||||
│ ├── resource-stats-card.example.tsx
|
||||
│ └── index.ts
|
||||
├── index.ts # Barrel exports
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
All shadcn components can be imported from `@/components/shadcn`:
|
||||
|
||||
```tsx
|
||||
import { Card, CardHeader, CardContent } from "@/components/shadcn";
|
||||
import { ResourceStatsCard } from "@/components/shadcn";
|
||||
```
|
||||
|
||||
### Adding New shadcn Components
|
||||
|
||||
When adding new shadcn components using the CLI:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add [component-name]
|
||||
```
|
||||
|
||||
The component will be automatically added to this directory due to the configuration in `components.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"aliases": {
|
||||
"ui": "@/components/shadcn"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component Guidelines
|
||||
|
||||
1. **shadcn base components** - Use as-is from shadcn/ui (e.g., `card.tsx`)
|
||||
2. **Custom components built on shadcn** - Create in subdirectories (e.g., `resource-stats-card/`)
|
||||
3. **CVA variants** - Use Class Variance Authority for type-safe variants
|
||||
4. **Theme support** - Include `dark:` classes for dark/light theme compatibility
|
||||
5. **TypeScript** - Always export types and use proper typing
|
||||
|
||||
### Resources
|
||||
|
||||
- [shadcn/ui Documentation](https://ui.shadcn.com)
|
||||
- [CVA Documentation](https://cva.style/docs)
|
||||
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
||||
|
||||
## Key Features
|
||||
|
||||
### Authentication System
|
||||
|
||||
- NextAuth.js with multiple providers (credentials, OAuth, SAML)
|
||||
- Server-side authentication with middleware protection
|
||||
- Session management and role-based access control
|
||||
|
||||
### Cloud Provider Management
|
||||
|
||||
- Multi-cloud support (AWS, Azure, GCP, GitHub, K8s, M365)
|
||||
- Credential management with secure forms
|
||||
- Connection testing and status monitoring
|
||||
|
||||
### Security Scanning
|
||||
|
||||
- Real-time scan progress monitoring
|
||||
- Bulk operations and filtering
|
||||
- Scheduled scan management
|
||||
- Download and export capabilities
|
||||
|
||||
### Compliance Frameworks
|
||||
|
||||
- 36+ compliance frameworks (CIS, NIST, PCI-DSS, etc.)
|
||||
- Interactive compliance reports with charts
|
||||
- Requirement-level drill-down views
|
||||
- Custom compliance mapping
|
||||
|
||||
### Lighthouse AI Assistant
|
||||
|
||||
- LangChain-powered security chatbot
|
||||
- Context-aware responses about findings
|
||||
- Integration with scan data and compliance frameworks
|
||||
|
||||
### Findings Management
|
||||
|
||||
- Advanced filtering and search
|
||||
- Mute/unmute functionality with reasons
|
||||
- Severity-based classification
|
||||
- Detailed finding analysis
|
||||
|
||||
## Patterns & Conventions
|
||||
|
||||
### Component Architecture
|
||||
|
||||
```typescript
|
||||
// Prefer server components when possible
|
||||
export default async function PageComponent() {
|
||||
const data = await fetchData();
|
||||
return <ClientComponent data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
```typescript
|
||||
// Use Zustand for global state
|
||||
import { useStore } from "@/hooks/use-store";
|
||||
|
||||
const { filters, setFilters } = useStore();
|
||||
```
|
||||
|
||||
### Server Actions
|
||||
|
||||
```typescript
|
||||
"use server";
|
||||
|
||||
export async function updateProvider(formData: FormData) {
|
||||
// Validate with Zod
|
||||
// Update via API
|
||||
// Revalidate cache
|
||||
}
|
||||
```
|
||||
|
||||
### Form Handling
|
||||
|
||||
```typescript
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
```
|
||||
|
||||
## File & Code Style
|
||||
|
||||
### Naming & Organization
|
||||
|
||||
- **Component Naming**: `PascalCase` for components, `camelCase` for helpers
|
||||
- **Foldering**: Colocate domain components under domain folders (e.g., `components/integrations/jira/`)
|
||||
- **Imports**: Honor alias paths (`@/components/...`). Keep import order consistent with ESLint rules
|
||||
- **CSS**: Prefer Tailwind classes; avoid ad-hoc CSS files unless justified
|
||||
|
||||
### Import Organization
|
||||
|
||||
```typescript
|
||||
// External libraries
|
||||
import React from "react";
|
||||
import { Button } from "@heroui/react";
|
||||
|
||||
// Internal utilities
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Types
|
||||
import type { ComponentProps } from "@/types";
|
||||
```
|
||||
|
||||
## Styling Guidelines
|
||||
|
||||
### Tailwind + shadcn/ui (New) / HeroUI (Existing)
|
||||
|
||||
- **Use shadcn/ui components for new UI features/pages** with the new Tailwind theme
|
||||
- Existing features/pages should continue using HeroUI (migrated from NextUI) for consistency
|
||||
- Custom Prowler color palette defined in tailwind.config.js
|
||||
- Dark/light theme support via next-themes
|
||||
- Custom shadows and animations for Prowler brand
|
||||
|
||||
## Library-Specific Guidelines
|
||||
|
||||
### Zod v4 (Schema Validation)
|
||||
|
||||
**Breaking changes from v3:**
|
||||
|
||||
- ❌ `.nonempty()` → ✅ `.min(1)` for strings
|
||||
- ❌ `z.string().email()` → ✅ `z.email()` (top-level function)
|
||||
- ❌ `z.string().uuid()` → ✅ `z.uuid()` (top-level function)
|
||||
- ❌ `z.string().url()` → ✅ `z.url()` (top-level function)
|
||||
- ❌ `required_error` parameter → ✅ `error` parameter
|
||||
- ❌ `message` parameter → ✅ `error` parameter
|
||||
- ⚠️ `.optional()` type inference changed - fields are now `T | undefined` in inferred types
|
||||
|
||||
**Example migration:**
|
||||
|
||||
```typescript
|
||||
// ❌ Zod v3
|
||||
const schema = z.object({
|
||||
email: z.string().email({ message: "Invalid email" }),
|
||||
name: z.string().nonempty("Required"),
|
||||
id: z.string().uuid(),
|
||||
});
|
||||
|
||||
// ✅ Zod v4
|
||||
const schema = z.object({
|
||||
email: z.email({ error: "Invalid email" }),
|
||||
name: z.string().min(1, "Required"),
|
||||
id: z.uuid(),
|
||||
});
|
||||
```
|
||||
|
||||
### Zustand v5 (State Management)
|
||||
|
||||
**Breaking changes from v4:**
|
||||
|
||||
- ✅ No API changes required for basic usage
|
||||
- ⚠️ `shallow` comparison must use `useShallow` hook from `zustand/react/shallow`
|
||||
- ⚠️ Selectors must return stable references to avoid infinite loops
|
||||
- ⚠️ `persist` middleware no longer auto-stores initial state - call `setState()` explicitly if needed
|
||||
|
||||
**Best practices:**
|
||||
|
||||
```typescript
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
const useStore = create(
|
||||
persist(
|
||||
(set) => ({
|
||||
value: 0,
|
||||
increment: () => set((state) => ({ value: state.value + 1 })),
|
||||
}),
|
||||
{ name: "my-store" },
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### AI SDK v5 (Chat & AI Features)
|
||||
|
||||
**Breaking changes from v4:**
|
||||
|
||||
- ❌ `Message` type → ✅ `UIMessage` type
|
||||
- ❌ `message.content` string → ✅ `message.parts` array structure
|
||||
- ❌ `handleSubmit` / `handleInputChange` → ✅ `sendMessage` with manual state
|
||||
- ❌ `append()` → ✅ `sendMessage({ text: "..." })`
|
||||
- ❌ `api: "/endpoint"` → ✅ `transport: new DefaultChatTransport({ api: "/endpoint" })`
|
||||
- ❌ `LangChainAdapter.toDataStreamResponse()` → ✅ `toUIMessageStream()` from `@ai-sdk/langchain`
|
||||
|
||||
**Example migration:**
|
||||
|
||||
```typescript
|
||||
// ❌ AI SDK v4
|
||||
import { useChat } from "ai";
|
||||
const { messages, handleSubmit, input, handleInputChange } = useChat({
|
||||
api: "/api/chat",
|
||||
});
|
||||
|
||||
// ✅ AI SDK v5
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
const { messages, sendMessage } = useChat({
|
||||
transport: new DefaultChatTransport({ api: "/api/chat" }),
|
||||
});
|
||||
// Manual input state management required
|
||||
const [input, setInput] = useState("");
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
sendMessage({ text: input });
|
||||
setInput("");
|
||||
};
|
||||
```
|
||||
|
||||
**UIMessage structure:**
|
||||
|
||||
```typescript
|
||||
// Message parts-based structure
|
||||
const message: UIMessage = {
|
||||
id: "msg-1",
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: "Hello world" }],
|
||||
};
|
||||
|
||||
// Extract text from parts
|
||||
const text = message.parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => ("text" in p ? p.text : ""))
|
||||
.join("");
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Playwright E2E Tests
|
||||
|
||||
- Add/update E2E tests for critical flows you modify
|
||||
- Scope: run only affected specs when iterating
|
||||
- Commit snapshot updates only with real UI changes
|
||||
- Determinism: avoid relying on real external services; mock or stub where possible
|
||||
|
||||
### Component Testing (Future)
|
||||
|
||||
- Jest + React Testing Library
|
||||
- Component unit tests
|
||||
- Integration tests for complex flows
|
||||
|
||||
## Performance
|
||||
|
||||
- Keep Client Components lean; avoid heavy client-side logic where server boundary is possible
|
||||
- Use streaming, partial rendering, and skeletons for long operations
|
||||
- **DO NOT use `useMemo` or `useCallback`** - React 19 Compiler handles this automatically
|
||||
- App Router with server components
|
||||
- Image optimization with next/image
|
||||
- Font optimization with next/font
|
||||
- Bundle analysis and code splitting
|
||||
|
||||
## Security
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Do not commit secrets or `.env.local` values. Use placeholders in examples
|
||||
- Avoid logging sensitive data. Sanitize error messages shown to users
|
||||
- Strict CSP headers in next.config.js
|
||||
- XSS protection and CSRF mitigation
|
||||
- Secure cookie configuration
|
||||
|
||||
### Data Handling
|
||||
|
||||
- Client-side validation with Zod
|
||||
- Server-side sanitization
|
||||
- Secure credential storage patterns
|
||||
|
||||
### Authentication
|
||||
|
||||
- NextAuth: use server helpers and `server-only` where appropriate
|
||||
- Protect routes through middleware and layout boundaries
|
||||
- JWT tokens via NextAuth
|
||||
- Automatic token refresh
|
||||
- Protected routes via middleware
|
||||
|
||||
## Quality Gates (before submitting changes) IMPORTANT!
|
||||
|
||||
1. `npm run typecheck` shows 0 new errors
|
||||
2. `npm run lint:check` passes or is fixed via `npm run lint:fix`
|
||||
3. `npm run format:check` passes or is corrected via `npm run format:write`
|
||||
4. Relevant Playwright specs pass locally
|
||||
5. UI states (loading, error, empty) are handled
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding New Pages
|
||||
|
||||
1. Create page component in `app/(prowler)/`
|
||||
2. Add route to navigation in `lib/menu-list.ts`
|
||||
3. Implement required server actions
|
||||
4. Add proper TypeScript types (using const-based type pattern). IMPORTANT!: avoid "any" type.
|
||||
|
||||
### Creating Components
|
||||
|
||||
1. **Use shadcn/ui for new UI components that belong to new features/pages**
|
||||
2. Existing features/pages should continue using HeroUI for consistency
|
||||
3. Follow established patterns in `components/shadcn/` for new components
|
||||
4. Implement proper TypeScript interfaces (using const-based type pattern)
|
||||
5. Add to component index files
|
||||
|
||||
### Integrating with Backend
|
||||
|
||||
1. Create server actions in `actions/`
|
||||
2. Define TypeScript types in `types/` (using const-based type pattern)
|
||||
3. Handle loading and error states
|
||||
4. Implement proper caching strategy
|
||||
|
||||
### When Implementing New UI
|
||||
|
||||
- **Use shadcn/ui components with the new Tailwind theme for new UI features/pages**
|
||||
- **For existing features/pages, continue using HeroUI components for consistency**
|
||||
- Start from existing patterns in the closest domain folder
|
||||
- Reuse primitives from `components/shadcn` (for new features) or `components/ui` (HeroUI for existing) and existing composables
|
||||
- Add types to `types/` if they're shared (2+ features); otherwise colocate types near usage (1 feature)
|
||||
- Follow The Scope Rule strictly - used by 2+ features = shared, 1 feature = local
|
||||
- Update feature docs and `ui/CHANGELOG.md` when behavior changes
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
```bash
|
||||
# Authentication
|
||||
NEXTAUTH_SECRET=your_secret_here
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# API Configuration
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080
|
||||
|
||||
# AI Features
|
||||
OPENAI_API_KEY=your_openai_key
|
||||
```
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. Copy `.env.example` to `.env.local`
|
||||
2. Configure authentication providers
|
||||
3. Set up API backend connection
|
||||
4. Install dependencies and start dev server
|
||||
|
||||
## Deployment
|
||||
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
npm run build # Build optimized bundle
|
||||
npm run start # Start production server
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
- Dockerfile available for containerization
|
||||
- Standalone output for minimal container size
|
||||
- Health checks via `/api/health` endpoint
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **TypeScript errors**: Run `npm run typecheck`
|
||||
2. **Lint issues**: Run `npm run lint:fix`
|
||||
3. **Build failures**: Check Next.js build logs
|
||||
4. **Authentication issues**: Verify NextAuth configuration
|
||||
|
||||
### Debug Tools
|
||||
|
||||
- Next.js built-in debugger
|
||||
- React Developer Tools
|
||||
- Network tab for API debugging
|
||||
- Lighthouse for performance analysis
|
||||
|
||||
## Recent Major Migrations (January 2025)
|
||||
|
||||
- ✅ React 18 → 19.1.1 (async components, useActionState, React Compiler)
|
||||
- ✅ Next.js 14 → 15.5.3 (enhanced App Router)
|
||||
- ✅ NextUI → HeroUI 2.8.4
|
||||
- ✅ Zod 3.25.73 → 4.1.11 (breaking: deprecated methods)
|
||||
- ✅ Zustand 4.5.7 → 5.0.8 (compatible)
|
||||
- ✅ AI SDK 4.3.16 → 5.0.59 (breaking: new message structure)
|
||||
- ✅ LangChain updates with new adapter patterns
|
||||
|
||||
## Summary
|
||||
|
||||
These guidelines ensure:
|
||||
|
||||
- ✅ Consistent code patterns across the project
|
||||
- ✅ Optimal performance with Next.js 15 and React 19 with Compiler
|
||||
- ✅ Clear, maintainable architecture following The Scope Rule
|
||||
- ✅ Proper separation of concerns
|
||||
- ✅ Type safety throughout the codebase using const-based types
|
||||
- ✅ Semantic Tailwind usage without var() or hex colors in className
|
||||
- ✅ Purposeful use of cn() only for conditionals and merging
|
||||
|
||||
**When in doubt, ask before deviating from these patterns.**
|
||||
|
||||
## References
|
||||
|
||||
- **High-level project guide**: `../AGENTS.md` (root Prowler project - takes priority)
|
||||
- **UI Changelog**: `./CHANGELOG.md`
|
||||
@@ -12,8 +12,11 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- React Compiler support for automatic optimization [(#8748)](https://github.com/prowler-cloud/prowler/pull/8748)
|
||||
- Turbopack support for faster development builds [(#8748)](https://github.com/prowler-cloud/prowler/pull/8748)
|
||||
- Add compliance name in compliance detail view [(#8775)](https://github.com/prowler-cloud/prowler/pull/8775)
|
||||
- Support C5 compliance framework for the AWS provider [(#8830)](https://github.com/prowler-cloud/prowler/pull/8830)
|
||||
- API key management in user profile [(#8308)](https://github.com/prowler-cloud/prowler/pull/8308)
|
||||
- Refresh access token error handling [(#8864)](https://github.com/prowler-cloud/prowler/pull/8864)
|
||||
- Support Common Cloud Controls for AWS, Azure and GCP [(#8000)](https://github.com/prowler-cloud/prowler/pull/8000)
|
||||
- New M365 credentials certificate authentication method [(#8929)](https://github.com/prowler-cloud/prowler/pull/8929)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -34,6 +37,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- ThreatScore for each pillar in Prowler ThreatScore specific view [(#8582)](https://github.com/prowler-cloud/prowler/pull/8582)
|
||||
- Remove maxTokens model param for GPT-5 models [(#8843)](https://github.com/prowler-cloud/prowler/pull/8843)
|
||||
- MITRE ATTACK compliance view now shows all requirements in charts [(#8886)](https://github.com/prowler-cloud/prowler/pull/8886)
|
||||
- Mutelist menu item now doesn't blink [(#8932)](https://github.com/prowler-cloud/prowler/pull/8932)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
SelectViaGCP,
|
||||
} from "@/components/providers/workflow/forms/select-credentials-type/gcp";
|
||||
import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github";
|
||||
import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365";
|
||||
import { getProviderFormType } from "@/lib/provider-helpers";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
@@ -28,6 +29,7 @@ export default async function AddCredentialsPage({ searchParams }: Props) {
|
||||
if (providerType === "gcp") return <SelectViaGCP initialVia={via} />;
|
||||
if (providerType === "github")
|
||||
return <SelectViaGitHub initialVia={via} />;
|
||||
if (providerType === "m365") return <SelectViaM365 initialVia={via} />;
|
||||
return null;
|
||||
|
||||
case "credentials":
|
||||
|
||||
@@ -28,31 +28,36 @@ export default async function Providers({
|
||||
<ContentLayout title="Cloud Providers" icon="lucide:cloud-cog">
|
||||
<FilterControls search customFilters={filterProviders || []} />
|
||||
<Spacer y={8} />
|
||||
<Suspense
|
||||
key={searchParamsKey}
|
||||
fallback={
|
||||
<>
|
||||
<div className="flex items-center gap-4 md:justify-end">
|
||||
<ManageGroupsButton />
|
||||
<MutedFindingsConfigButton />
|
||||
<AddProviderButton />
|
||||
</div>
|
||||
<Spacer y={8} />
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12">
|
||||
<SkeletonTableProviders />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ProvidersContent searchParams={resolvedSearchParams} />
|
||||
<ProvidersActions />
|
||||
<Spacer y={8} />
|
||||
<Suspense key={searchParamsKey} fallback={<ProvidersTableFallback />}>
|
||||
<ProvidersTable searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const ProvidersContent = async ({
|
||||
const ProvidersActions = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-4 md:justify-end">
|
||||
<ManageGroupsButton />
|
||||
<MutedFindingsConfigButton />
|
||||
<AddProviderButton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProvidersTableFallback = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12">
|
||||
<SkeletonTableProviders />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProvidersTable = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
@@ -97,13 +102,6 @@ const ProvidersContent = async ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-4 md:justify-end">
|
||||
<ManageGroupsButton />
|
||||
<MutedFindingsConfigButton />
|
||||
<AddProviderButton />
|
||||
</div>
|
||||
<Spacer y={8} />
|
||||
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12">
|
||||
<DataTable
|
||||
|
||||
+7
-1
@@ -7,12 +7,18 @@ interface ComplianceAccordionRequirementTitleProps {
|
||||
}
|
||||
|
||||
export const ComplianceAccordionRequirementTitle = ({
|
||||
type,
|
||||
name,
|
||||
status,
|
||||
}: ComplianceAccordionRequirementTitleProps) => {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex w-5/6 items-center gap-1">
|
||||
<div className="flex w-5/6 items-center gap-2">
|
||||
{type && (
|
||||
<span className="bg-primary/10 text-primary rounded-md px-2 py-0.5 text-xs font-medium">
|
||||
{type}
|
||||
</span>
|
||||
)}
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
<StatusFindingBadge status={status} />
|
||||
|
||||
@@ -118,11 +118,13 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
onClick={navigateToDetail}
|
||||
>
|
||||
<div className="flex w-full items-center gap-4">
|
||||
<Image
|
||||
src={getComplianceIcon(title)}
|
||||
alt={`${title} logo`}
|
||||
className="h-10 w-10 min-w-10 rounded-md border border-gray-300 bg-white object-contain p-1"
|
||||
/>
|
||||
{getComplianceIcon(title) && (
|
||||
<Image
|
||||
src={getComplianceIcon(title)}
|
||||
alt={`${title} logo`}
|
||||
className="h-10 w-10 min-w-10 rounded-md border border-gray-300 bg-white object-contain p-1"
|
||||
/>
|
||||
)}
|
||||
<div className="flex w-full flex-col">
|
||||
<h4 className="text-small mb-1 leading-5 font-bold">
|
||||
{formatTitle(title)}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
import {
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
} from "./shared-components";
|
||||
|
||||
export const C5CustomDetails = ({
|
||||
requirement,
|
||||
}: {
|
||||
requirement: Requirement;
|
||||
}) => {
|
||||
const sections = [
|
||||
{
|
||||
title: "Description",
|
||||
content: requirement.description,
|
||||
},
|
||||
{
|
||||
title: "About Criteria",
|
||||
content: requirement.about_criteria as string | undefined,
|
||||
},
|
||||
{
|
||||
title: "Complementary Criteria",
|
||||
content: requirement.complementary_criteria as string | undefined,
|
||||
},
|
||||
].filter((section) => section.content);
|
||||
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
{sections.map((section) => (
|
||||
<ComplianceDetailSection key={section.title} title={section.title}>
|
||||
<ComplianceDetailText>{section.content}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
))}
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { cn } from "@/lib";
|
||||
import { CCC_MAPPING_SECTIONS, CCC_TEXT_SECTIONS } from "@/lib/compliance/ccc";
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
import {
|
||||
ComplianceBadge,
|
||||
ComplianceBadgeContainer,
|
||||
ComplianceChipContainer,
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
} from "./shared-components";
|
||||
|
||||
interface CCCDetailsProps {
|
||||
requirement: Requirement;
|
||||
}
|
||||
|
||||
export const CCCCustomDetails = ({ requirement }: CCCDetailsProps) => {
|
||||
// Map text sections with requirement data
|
||||
const textSections = CCC_TEXT_SECTIONS.map((section) => ({
|
||||
...section,
|
||||
content: requirement[section.key] as string | undefined,
|
||||
})).filter((section) => section.content);
|
||||
|
||||
// Map mapping sections with requirement data
|
||||
const mappingSections = CCC_MAPPING_SECTIONS.map((section) => ({
|
||||
...section,
|
||||
data: requirement[section.key] as Array<{
|
||||
ReferenceId: string;
|
||||
Identifiers: string[];
|
||||
}>,
|
||||
})).filter((section) => section.data);
|
||||
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
{textSections.map((section) => (
|
||||
<ComplianceDetailSection key={section.title} title={section.title}>
|
||||
<ComplianceDetailText className={section.className}>
|
||||
{section.content}
|
||||
</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
))}
|
||||
|
||||
{requirement.family_name && (
|
||||
<ComplianceBadgeContainer>
|
||||
<ComplianceBadge
|
||||
label="Family"
|
||||
value={requirement.family_name as string}
|
||||
color="purple"
|
||||
/>
|
||||
</ComplianceBadgeContainer>
|
||||
)}
|
||||
|
||||
{requirement.applicability && (
|
||||
<ComplianceChipContainer
|
||||
title="Applicability"
|
||||
items={requirement.applicability as string[]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mappingSections.map((section) => (
|
||||
<ComplianceDetailSection key={section.title} title={section.title}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{section.data.map((mapping, index) => (
|
||||
<div key={index} className="flex flex-col gap-1">
|
||||
<span className="text-muted-foreground text-xs font-medium">
|
||||
{mapping.ReferenceId}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mapping.Identifiers.map((identifier, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||||
section.colorClasses,
|
||||
)}
|
||||
>
|
||||
{identifier}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ComplianceDetailSection>
|
||||
))}
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
import AWSLogo from "./aws.svg";
|
||||
import C5Logo from "./c5.svg";
|
||||
import CCCLogo from "./ccc.svg";
|
||||
import CISLogo from "./cis.svg";
|
||||
import CISALogo from "./cisa.svg";
|
||||
import ENSLogo from "./ens.png";
|
||||
@@ -17,59 +19,32 @@ import PROWLERTHREATLogo from "./prowlerThreat.svg";
|
||||
import RBILogo from "./rbi.svg";
|
||||
import SOC2Logo from "./soc2.svg";
|
||||
|
||||
const COMPLIANCE_LOGOS = {
|
||||
aws: AWSLogo,
|
||||
cisa: CISALogo,
|
||||
cis: CISLogo,
|
||||
ens: ENSLogo,
|
||||
ffiec: FFIECLogo,
|
||||
fedramp: FedRAMPLogo,
|
||||
gdpr: GDPRLogo,
|
||||
gxp: GxPLogo,
|
||||
hipaa: HIPAALogo,
|
||||
iso: ISOLogo,
|
||||
mitre: MITRELogo,
|
||||
nist: NISTLogo,
|
||||
pci: PCILogo,
|
||||
rbi: RBILogo,
|
||||
soc2: SOC2Logo,
|
||||
kisa: KISALogo,
|
||||
prowlerthreatscore: PROWLERTHREATLogo,
|
||||
nis2: NIS2Logo,
|
||||
c5: C5Logo,
|
||||
ccc: CCCLogo,
|
||||
} as const;
|
||||
|
||||
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;
|
||||
}
|
||||
if (complianceTitle.toLowerCase().includes("kisa")) {
|
||||
return KISALogo;
|
||||
}
|
||||
if (complianceTitle.toLowerCase().includes("prowlerthreatscore")) {
|
||||
return PROWLERTHREATLogo;
|
||||
}
|
||||
if (complianceTitle.toLowerCase().includes("nis2")) {
|
||||
return NIS2Logo;
|
||||
}
|
||||
const lowerTitle = complianceTitle.toLowerCase();
|
||||
return Object.entries(COMPLIANCE_LOGOS).find(([keyword]) =>
|
||||
lowerTitle.includes(keyword),
|
||||
)?.[1];
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="894" height="854" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<desc>PNG to SVG conversion by ConvertICO.com</desc>
|
||||
<image x="0" y="0" width="894" height="854" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA34AAANWCAMAAACWCK59AAAAwFBMVEVEQkSnpqf00mSoXkTY19jN6vT87b6kkox3dnf0pYnv7u92T0Tx+fv0vlzRaUTsglpeXV785KHUnoz81sv8+OX0vKdkSkT80qSOj47Av8DsckT0spj87OX84tr0yrn82oDg8viUgnyOVkT0kmvMkny/ZERRUVGxsrHg4eCAgYBqamqZmpnKysr0zmTsikz0mHf88s783o3seE30yoxURkS0YkSEU0TcbUT8/vxsTkScWkTsjGb86qz0wqz88u30qow2jTRCAAAACXBIWXMAABYlAAAWJQFJUiTwAAAgAElEQVR42u2dC2PaSLK2RQa8oOkv4AVyIFyihI0kLntwOE6ijG35//+rTy2BjRNodUvdUlWr2s7M7Dkz4wno4a1669JORAfqYfzr9Xj8+/h18fv4dX4i/k2vJNjj0EsAGb8z9jx2hh+7Dt/LnzL8iD7Cj44Ubmeoea+nc/pKjnf6vnhe/5b0i3+/HNc7k0R6rQk/Om/Ziy7Cl2F0guoNfN4bHF8gPYfvDMDXwJQkkfCjc9K7M9qu6pqek7H5iuORRjqEX2Nzu3O163jnIqYdvM65Ir5RQzqEX/MUz7DaSVF5nhnSu0P4NUPxfkvs6kDvRQ9ftJB0kPCzHDrtgaUmGXxxSkkHCT+LLRb+nHc8aAi+CCHpIOFnFXYg0rwCJGbWDL2LhJ8VooeDwBc75oQfAUj44ePuNdPDe0gFCT+M9EXnMaeHlr0OqSDhh032PA8vc6SChB9e+rjqud4JQM8a+F7wo35Rwg+wxWkPdAIVJA0k/MCp3qvV4tkLX+dVBekQfhB07whec86LCBKEhB+IbK9jedj5R6/okT86hF+tutfpNEn4/jBkXNJAwq8u3bOwyKA6uss8ehYIv4ptTvwdLRrTwI5HZgzhV7XP2Wk8gN4RQEZ5IOFXYb5HundJBakgSPhVke8RfhdLEcQf4Wc24SPw8rzQtChPh/AzFHYSgPnleDqEn1b0mtXXUppCckIJP20ZH+me+nAS4Uf46dM9Qk8NwqMVQ48Q4VfK6STDpVhLDDVlE36llY9CzvLFQEKQ8CukfCR8GgoRFIESfqrC5zHCTm8/DD1VhJ902HkaZqBDWSDhV23GR8pnxAklBST85DI+Ej7tWSA1oxF+cl4nHYMKSAwSfkLlIwANNqNRNwzhd8XtJOyoFEH41UYf8UdlCMKvJuUj9qoMQolAwu+U89ECCfJgCL/agk4iop4gtPE2jNN4+qixsy4FJBe02fgxcjtrR5ARfo3N+Uj5ag9Am62AToOlzwbl85gNNkxjEXSaS58Vbif27nCv2RGoQ8qHWvt40cSOOiDhR8qHTvuiyIIPkiwAJfxI+bBpHz/49a+xCug0Uvns4M873qxnxW+nmR5ow/BjiVJYo32vvy0b9C9zYRqGYLPwY5E9HWbnj6olRlLzAtDm4HdscfGsge/NsQdAzyP8LLVc7Gkx+z1Ms8ZOatYshNMg+Cxq1/ojSGORRVF1c0JQp0HSZ1F3559lMouWIzaoE9RpivLZ1Fh9UR1s0nev0xAAG4FfZBd+XudicMasiq6bEYA61isfs22k6OqDyawCsBFVCOvVz6JC++lcx69jlwB6pH7kdyLRPvsA7NgvgHbjlyqf1yT8rHKY7E8BLcbPxhVmuTVp6/TPbgW0Fz/b/E5x3veq95b9lu2uAdqKH2M2rlBiEmaEFdMPv3swjPBD5nfauEJQJgyz74PH4gzQsVL57NzeKduMbOHv3tYQ1Fb8bOSPSdbBrAs/MwEk9aOsr84nUP7z30IA7STQOvwszfrktc/S8NPODNCxTPlsvbNB9aPfSv2zbw7CsU37LOVP9cGz8mWwL/50rNK+jrXap/6pb6n7axeBjk30WXtbWIFnjhF+hF+17FlLXzHPwVIAOxYNIjkWaV/HXu0rgh+zNg0m/Ig96Npn8avidWypQdiCX4dCz+bonz0ZoA34ecziC6JLbZ31rI0KLBFAxwbps/h+dq9T6imzuBRjRQnewc+exfB1OmU/4u2thVrRhYZe/ewt9pXL+xpgSlmQADrItc9m5ePax+g10l+SIfw0hp42a5+GCpdns/51mEf41ed4Wn10PVo2v07IE0AHt/RZHXrqerCsDj87qO8DdBCzZ7f2ycDnukHgsobrH+YpQLTqZ7v08dAz96FiCX6x2/DGBMwOqEPaB1X7cp+pmLVag0FrG8USAHp26x/SAJTwg6p9+c8Ti1vbwWC7lQm97I4VCD+qtFceTgXBYOA4A7/VomC9fH8Q4SdfybI99JQxE+IEP99xfDn8Irub85DOQODDz2uA9smEk7vdJtE+fgaD6VQSQJtfNYwzEOjwY57ljWaSoWe82w02Z/jFEq+c3fhhnIHAhZ/t8w3y1fbdrtt1Xs5ms9uRYYWwAuhg074m2C5MRvsS/Hw1/Kz3rPAlgA5pH76C32/a5yQcdrukfwhLEOjws52+jpT2tc+1j/OX4hdLvIK204cr/HQwwdex/khpX7u9fgvfK4D0EYZL/xDh5zUh9JRIXuJ2u7t2nAv4tWX0r2N9/ucRftTpYsrzDMPJxHcuHH/dbbdl9M/6wg2eIVws+Nlf7etITrcn+K0v4ec767UMfp71QbyHpwHNwaJ9DRA/Gfhcd7v1L4ofB3AyCUPK/xDpn0Pah0v73MlV/BxfDr8m9A0hyf8w4NcI6ZO6t8cN+YiRc/X4/nbryuifZ3/+hwJABPixRmif1HR76G5bufhJTb/bX0BFUQF04MPXBOmT3Owihi9rv962Qrn1E9YbMAguIoOOH2tI6Mkk+Evx8/Pwa8nh55H+EX4S8DVE+yTga+Vq3xFAuQC0Cfkf9A4Y4Pg1ocm6I9fswreayeG3bbly/mcT7CzCj7Sv/EPC4ul0IwHfUf8CCf1rRAM78AqEA137OqR9aejJEvw2mvFrgKMMPPx0SPsQPCAx+33CT3h8vn5CYvt1MxppIQPokPYheDxYvJsq4OdsNsGUyW1/aYD9EhF+pH3FC36Mr5ZY3/uOwul2pzsWk/4B1z+HtK/mT2eJkgOb7tZrRwU/f8PxY/QhB9x+cQg+6AW/RPvWXf/eUTp8/Gi3c2N6pUHrH2D8mtFnLaFQCX6KoWc2fLuT0b+m4EfqR9qnnvelm10myvCl7dccwJgAhKx/INWvOfjlNwXH7fbntV8EvzT8ZLT9pQO4/cwh7YOc90VheH26XUL/1u0drV866R/AAoRD2gc6IQnDSTn82vk/xGMNSbMJP9I+Ne1r5Y4YiQGcTOS2n3mkf4Qfad8f+G1L4xfGEvrXIf0j/CLyPH/XvoFT6viD7Za2n4EFEBZ+jWmz7kiHnhXh55H90nj8KO87bzVL4bt3yp7BRm79BOV/DcevSdonEXry5RIbpzx+fPuL1PJB6n9pMn5NGjCS2OwSBDrgUwKwEa1+oPTPgaV9lP6f4bfREHqm03+S288aIYCwFmA7kLSP4Du9GPGUj9fqoS8FMGi5LkUg4PTPgaR9VHs6aR9L8Ns4jj78WoGk/cIacIciHP2DgV9TBozk7t3h0+0FRozy1k8ELpN7Kyj/a5r6NcZ2kwo9Xb5cwtGN31Qy/CT9axh+zfA8PSb1mcu3mq3Xvlb4ssunpxLT75kAkv41CL9m3OPgKYWe2vHLli9JPXJNmIAAon8Q8KNuw3PxS+D7rB8+3n7dldr+0pQOGBD33wLArxHdTtLpBmvvJmsT+Dl+unyJSb8lVr8pQPZ/1o9fM676kO20b7cnEyPwHcdvZfUvagCAAPQPAH5eEwId2dDTPH6S/yXM/o9FRurXhGt2GJOFr8xmF+36l5YASf+sxs/+KhN/kyU/Zlk7wW9gED+V/C+hj783Nr879dufTt3aZzt88vPVrN02DN9rACqvf3bnf3UDWDN+9mufJx15JnnfdlJ2ul17+Gk7f83Fz374pC0X3uvCTRfD8L0GoLL6lwYoHumflfjZn9rL4sfi9q4a/HyV/I93JNntgNa7fMmpEz7b4xrpdzbt85z4vlPJUQPQ9neq1u1nNaqf7fh15PHjfZ6Gel2u3f4wjV3ZN8qzugRfa/TpEHw1Z318uNZMk7W4/3MqN4DUAAGsEcC68LN7ut3rKESekRtPweNnswFTY/enQ9pnJpuQh8/le12qhC8LQDfdoOWGkZIAevYC2Cz8mN3ZhMKnqesG067m1RJSt79vAqn9L28yQHrDbMDPYvjkOzwz+MJWa7OpHL5U/wabltQC+iakgDV1f9aDn721pLSNVwE/vst603VqwO+0/zNWe988a9+1puBnb7cLU4ti4rDd2g42Tm1nMNhu221X6b2zUwC9esaPnFq0z7NW+ZTeRJbe3ze4rxk/FivlDXY6MPVUH5watM9S+DymJn1st5t89muET70F22YPtBb/xaHQU9e7p/j2pZ0uxicc8u//U5iAP8fPuvewlvCzcvysTB08jymWbnm1b931nfqP73e7QeC6ygmEfQBWX36vGj87u11UA88X/ADw5zspfqHyp6h1b2QN6Z9D2qcj6lR83+qr9l3pgNlsW2EYq2YRtr2bXuXdLw5pX+X4xSx0W9vNBgh9PAHM8GPK9FnHn934Wdbtkg4VqUYsbLfb1m65/FGC8D9PFGbgXxG0SgG9qgGsFj/bul08T92ujqPd7l+TgQ8Nv0mCn6v8eZrWIDyL3lBmLX52lRwKKV/WYA3CcrnQhF3AAeUJhU1lwIrTv2rx82xTPuXXwGVBABO/zAEN3QJvq03T8F6l3Z9OlfBZ9BlZRPmiiDeZbQYQpe+sCW0XFSPQEgQ9O/GLmGeV8BXAr/YWa7ke0Lgwf7Z8tNqHnzXaV1T50kL72vfvHcDH5z0wSmsofnNBPTv0j1mGny0Vv2yqoLine truncated
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,32 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 170" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="cccGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#0586BF"/>
|
||||
<stop offset="100%" style="stop-color:#00BCE7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Left C -->
|
||||
<g>
|
||||
<circle cx="81" cy="85" r="75" fill="url(#cccGradient)"/>
|
||||
<circle cx="81" cy="85" r="45" fill="white"/>
|
||||
</g>
|
||||
|
||||
<!-- Connecting pill 1 -->
|
||||
<ellipse cx="140" cy="85" rx="35" ry="16" fill="url(#cccGradient)"/>
|
||||
|
||||
<!-- Middle C -->
|
||||
<g>
|
||||
<circle cx="200" cy="85" r="75" fill="url(#cccGradient)"/>
|
||||
<circle cx="200" cy="85" r="45" fill="white"/>
|
||||
</g>
|
||||
|
||||
<!-- Connecting pill 2 -->
|
||||
<ellipse cx="260" cy="85" rx="35" ry="16" fill="url(#cccGradient)"/>
|
||||
|
||||
<!-- Right C -->
|
||||
<g>
|
||||
<circle cx="319" cy="85" r="75" fill="url(#cccGradient)"/>
|
||||
<circle cx="319" cy="85" r="45" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 944 B |
@@ -83,6 +83,7 @@ export const DeleteForm = ({
|
||||
variant="solid"
|
||||
color="danger"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <DeleteIcon size={24} />}
|
||||
>
|
||||
|
||||
@@ -78,6 +78,7 @@ export const DeleteGroupForm = ({
|
||||
variant="solid"
|
||||
color="danger"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <DeleteIcon size={24} />}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
|
||||
import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp";
|
||||
import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github";
|
||||
import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
interface UpdateCredentialsInfoProps {
|
||||
@@ -24,6 +25,9 @@ export const CredentialsUpdateInfo = ({
|
||||
if (providerType === "github") {
|
||||
return <SelectViaGitHub initialVia={initialVia} />;
|
||||
}
|
||||
if (providerType === "m365") {
|
||||
return <SelectViaM365 initialVia={initialVia} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ export const DeleteForm = ({
|
||||
variant="solid"
|
||||
color="danger"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <DeleteIcon size={24} />}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { SettingsIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { CustomAlertModal, CustomButton } from "@/components/ui/custom";
|
||||
import { useUIStore } from "@/store/ui/store";
|
||||
@@ -8,13 +10,38 @@ import { useUIStore } from "@/store/ui/store";
|
||||
import { MutedFindingsConfigForm } from "./forms";
|
||||
|
||||
export const MutedFindingsConfigButton = () => {
|
||||
const pathname = usePathname();
|
||||
const {
|
||||
isMutelistModalOpen,
|
||||
openMutelistModal,
|
||||
closeMutelistModal,
|
||||
hasProviders,
|
||||
shouldAutoOpenMutelist,
|
||||
resetMutelistModalRequest,
|
||||
} = useUIStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAutoOpenMutelist) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname !== "/providers") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasProviders) {
|
||||
openMutelistModal();
|
||||
}
|
||||
|
||||
resetMutelistModalRequest();
|
||||
}, [
|
||||
hasProviders,
|
||||
openMutelistModal,
|
||||
pathname,
|
||||
resetMutelistModalRequest,
|
||||
shouldAutoOpenMutelist,
|
||||
]);
|
||||
|
||||
const handleOpenModal = () => {
|
||||
if (hasProviders) {
|
||||
openMutelistModal();
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
GCPDefaultCredentials,
|
||||
GCPServiceAccountKey,
|
||||
KubernetesCredentials,
|
||||
M365Credentials,
|
||||
M365CertificateCredentials,
|
||||
M365ClientSecretCredentials,
|
||||
ProviderType,
|
||||
} from "@/types";
|
||||
|
||||
@@ -26,10 +27,13 @@ import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credenti
|
||||
import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credentials-type/aws-role-credentials-form";
|
||||
import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type";
|
||||
import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type/gcp-service-account-key-form";
|
||||
import {
|
||||
M365CertificateCredentialsForm,
|
||||
M365ClientSecretCredentialsForm,
|
||||
} from "./select-credentials-type/m365";
|
||||
import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
|
||||
import { GitHubCredentialsForm } from "./via-credentials/github-credentials-form";
|
||||
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
|
||||
import { M365CredentialsForm } from "./via-credentials/m365-credentials-form";
|
||||
|
||||
type BaseCredentialsFormProps = {
|
||||
providerType: ProviderType;
|
||||
@@ -103,11 +107,22 @@ export const BaseCredentialsForm = ({
|
||||
control={form.control as unknown as Control<AzureCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "m365" && (
|
||||
<M365CredentialsForm
|
||||
control={form.control as unknown as Control<M365Credentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "m365" &&
|
||||
searchParamsObj.get("via") === "app_client_secret" && (
|
||||
<M365ClientSecretCredentialsForm
|
||||
control={
|
||||
form.control as unknown as Control<M365ClientSecretCredentials>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{providerType === "m365" &&
|
||||
searchParamsObj.get("via") === "app_certificate" && (
|
||||
<M365CertificateCredentialsForm
|
||||
control={
|
||||
form.control as unknown as Control<M365CertificateCredentials>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{providerType === "gcp" &&
|
||||
searchParamsObj.get("via") === "service-account" && (
|
||||
<GCPServiceAccountKeyForm
|
||||
@@ -159,6 +174,15 @@ export const BaseCredentialsForm = ({
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
endContent={!isLoading && <ChevronRightIcon size={24} />}
|
||||
onPress={(e) => {
|
||||
const formElement = e.target as HTMLElement;
|
||||
const form = formElement.closest("form");
|
||||
if (form) {
|
||||
form.dispatchEvent(
|
||||
new Event("submit", { bubbles: true, cancelable: true }),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>{submitButtonText}</span>}
|
||||
</CustomButton>
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export { M365CertificateCredentialsForm } from "./m365-certificate-credentials-form";
|
||||
export { M365ClientSecretCredentialsForm } from "./m365-client-secret-credentials-form";
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { CustomInput, CustomTextarea } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { M365CertificateCredentials } from "@/types";
|
||||
|
||||
export const M365CertificateCredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<M365CertificateCredentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
App Certificate Credentials
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Please provide your Microsoft 365 application credentials with
|
||||
certificate authentication.
|
||||
</div>
|
||||
</div>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="tenant_id"
|
||||
type="text"
|
||||
label="Tenant ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Tenant ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.tenant_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_id"
|
||||
type="text"
|
||||
label="Client ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_id}
|
||||
/>
|
||||
<CustomTextarea
|
||||
control={control}
|
||||
name="certificate_content"
|
||||
label="Certificate Content"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the base64 encoded certificate content"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.certificate_content}
|
||||
minRows={4}
|
||||
/>
|
||||
<p className="text-default-500 text-sm">
|
||||
The certificate content must be base64 encoded from an unsigned
|
||||
certificate. For detailed instructions on how to generate and encode
|
||||
your certificate, please refer to the{" "}
|
||||
<CustomLink
|
||||
href="https://docs.prowler.com/user-guide/providers/microsoft365/authentication#generate-the-certificate"
|
||||
size="sm"
|
||||
>
|
||||
certificate generation guide
|
||||
</CustomLink>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { M365ClientSecretCredentials } from "@/types";
|
||||
|
||||
export const M365ClientSecretCredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<M365ClientSecretCredentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
App Client Secret Credentials
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Please provide your Microsoft 365 application credentials.
|
||||
</div>
|
||||
</div>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="tenant_id"
|
||||
type="text"
|
||||
label="Tenant ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Tenant ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.tenant_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_id"
|
||||
type="text"
|
||||
label="Client ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_secret"
|
||||
type="password"
|
||||
label="Client Secret"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client Secret"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_secret}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
M365CertificateCredentialsForm,
|
||||
M365ClientSecretCredentialsForm,
|
||||
} from "./credentials-type";
|
||||
export { SelectViaM365 } from "./select-via-m365";
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { RadioGroup } from "@heroui/radio";
|
||||
import React from "react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
|
||||
import { CustomRadio } from "@/components/ui/custom";
|
||||
import { FormMessage } from "@/components/ui/form";
|
||||
|
||||
type RadioGroupM365ViaCredentialsFormProps = {
|
||||
control: Control<any>;
|
||||
isInvalid: boolean;
|
||||
errorMessage?: string;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
export const RadioGroupM365ViaCredentialsTypeForm = ({
|
||||
control,
|
||||
isInvalid,
|
||||
errorMessage,
|
||||
onChange,
|
||||
}: RadioGroupM365ViaCredentialsFormProps) => {
|
||||
return (
|
||||
<Controller
|
||||
name="m365CredentialsType"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<RadioGroup
|
||||
className="flex flex-wrap"
|
||||
isInvalid={isInvalid}
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-default-500 text-sm">
|
||||
Select Authentication Method
|
||||
</span>
|
||||
<CustomRadio
|
||||
description="Connect using Application Client Secret"
|
||||
value="app_client_secret"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-2">App Client Secret Credentials</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio
|
||||
description="Connect using Application Certificate"
|
||||
value="app_certificate"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-2">App Certificate Credentials</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{errorMessage && (
|
||||
<FormMessage className="text-system-error dark:text-system-error">
|
||||
{errorMessage}
|
||||
</FormMessage>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { Form } from "@/components/ui/form";
|
||||
|
||||
import { RadioGroupM365ViaCredentialsTypeForm } from "./radio-group-m365-via-credentials-type-form";
|
||||
|
||||
interface SelectViaM365Props {
|
||||
initialVia?: string;
|
||||
}
|
||||
|
||||
export const SelectViaM365 = ({ initialVia }: SelectViaM365Props) => {
|
||||
const router = useRouter();
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
m365CredentialsType: initialVia || "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSelectionChange = (value: string) => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("via", value);
|
||||
router.push(url.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<RadioGroupM365ViaCredentialsTypeForm
|
||||
control={form.control}
|
||||
isInvalid={!!form.formState.errors.m365CredentialsType}
|
||||
errorMessage={form.formState.errors.m365CredentialsType?.message}
|
||||
onChange={handleSelectionChange}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./azure-credentials-form";
|
||||
export * from "./github-credentials-form";
|
||||
export * from "./k8s-credentials-form";
|
||||
export * from "./m365-credentials-form";
|
||||
@@ -1,109 +0,0 @@
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { M365Credentials } from "@/types";
|
||||
|
||||
export const M365CredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<M365Credentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
Connect via Credentials
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Please provide the information for your Microsoft 365 credentials.
|
||||
</div>
|
||||
</div>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_id"
|
||||
type="text"
|
||||
label="Client ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_secret"
|
||||
type="password"
|
||||
label="Client Secret"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client Secret"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_secret}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="tenant_id"
|
||||
type="text"
|
||||
label="Tenant ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Tenant ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.tenant_id}
|
||||
/>
|
||||
<p className="text-default-500 text-sm">
|
||||
{" "}
|
||||
User and password authentication is being deprecated due to
|
||||
Microsoft's on-going MFA enforcement across all tenants (see{" "}
|
||||
<CustomLink
|
||||
href="https://azure.microsoft.com/en-us/blog/announcing-mandatory-multi-factor-authentication-for-azure-sign-in/"
|
||||
size="sm"
|
||||
>
|
||||
Microsoft docs
|
||||
</CustomLink>
|
||||
).
|
||||
</p>
|
||||
|
||||
<div className="border-system-warning bg-system-warning-medium dark:text-default-300 flex items-center rounded-lg border p-2 text-sm">
|
||||
<InfoIcon className="mr-2 inline h-4 w-4 shrink-0" />
|
||||
<p className="text-xs font-extrabold">
|
||||
By October 2025, MFA will be mandatory.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-default-500 text-sm">
|
||||
Due to that change, you must only{" "}
|
||||
<CustomLink
|
||||
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/microsoft365/getting-started-m365/#step-3-configure-your-m365-account"
|
||||
size="sm"
|
||||
>
|
||||
use application authentication
|
||||
</CustomLink>{" "}
|
||||
to maintain all Prowler M365 scan capabilities.
|
||||
</p>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="user"
|
||||
type="text"
|
||||
label="User"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the User"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!control._formState.errors.user}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Password"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!control._formState.errors.password}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -75,6 +75,7 @@ export const DeleteRoleForm = ({
|
||||
variant="solid"
|
||||
color="danger"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <DeleteIcon size={24} />}
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ const alertVariants = cva(
|
||||
variant: {
|
||||
default: "bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50",
|
||||
destructive:
|
||||
"bg-danger-50 border-red-500/50 text-red-200 dark:border-red-500 dark:border-red-900/50 dark:text-red-200 dark:dark:border-red-900",
|
||||
"bg-danger-50 border-red-500/50 text-red-700 dark:border-red-500 dark:border-red-900/50 dark:text-red-700 dark:dark:border-red-900",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
+14
-3
@@ -1,3 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { CustomButton } from "@/components/ui/custom/custom-button";
|
||||
|
||||
interface ModalButtonsProps {
|
||||
@@ -7,6 +9,7 @@ interface ModalButtonsProps {
|
||||
isDisabled?: boolean;
|
||||
submitText?: string;
|
||||
submitColor?: "action" | "danger";
|
||||
submitIcon?: ReactNode;
|
||||
}
|
||||
|
||||
export const ModalButtons = ({
|
||||
@@ -16,24 +19,32 @@ export const ModalButtons = ({
|
||||
isDisabled = false,
|
||||
submitText = "Save",
|
||||
submitColor = "action",
|
||||
submitIcon,
|
||||
}: ModalButtonsProps) => {
|
||||
return (
|
||||
<div className="flex w-full justify-end gap-3 pt-4">
|
||||
<div className="flex w-full justify-center gap-6">
|
||||
<CustomButton
|
||||
size="lg"
|
||||
radius="lg"
|
||||
variant="faded"
|
||||
type="button"
|
||||
ariaLabel="Cancel"
|
||||
color="transparent"
|
||||
variant="light"
|
||||
className="w-full bg-transparent"
|
||||
onPress={onCancel}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
size="lg"
|
||||
radius="lg"
|
||||
className="w-full"
|
||||
ariaLabel={submitText}
|
||||
color={submitColor}
|
||||
onPress={onSubmit}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isDisabled || isLoading}
|
||||
startContent={submitIcon}
|
||||
>
|
||||
{submitText}
|
||||
</CustomButton>
|
||||
@@ -8,7 +8,7 @@ import { SaveIcon } from "@/components/icons";
|
||||
import { CustomButton } from "../custom";
|
||||
|
||||
interface FormCancelButtonProps {
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setIsOpen?: Dispatch<SetStateAction<boolean>>;
|
||||
onCancel?: () => void;
|
||||
children?: React.ReactNode;
|
||||
leftIcon?: React.ReactNode;
|
||||
@@ -19,10 +19,11 @@ interface FormSubmitButtonProps {
|
||||
loadingText?: string;
|
||||
isDisabled?: boolean;
|
||||
rightIcon?: React.ReactNode;
|
||||
color?: SubmitColorsType;
|
||||
}
|
||||
|
||||
interface FormButtonsProps {
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setIsOpen?: Dispatch<SetStateAction<boolean>>;
|
||||
onCancel?: () => void;
|
||||
submitText?: string;
|
||||
cancelText?: string;
|
||||
@@ -30,8 +31,16 @@ interface FormButtonsProps {
|
||||
isDisabled?: boolean;
|
||||
rightIcon?: React.ReactNode;
|
||||
leftIcon?: React.ReactNode;
|
||||
submitColor?: SubmitColorsType;
|
||||
}
|
||||
|
||||
export const SubmitColors = {
|
||||
action: "action",
|
||||
danger: "danger",
|
||||
} as const;
|
||||
|
||||
export type SubmitColorsType = (typeof SubmitColors)[keyof typeof SubmitColors];
|
||||
|
||||
const FormCancelButton = ({
|
||||
setIsOpen,
|
||||
onCancel,
|
||||
@@ -41,7 +50,7 @@ const FormCancelButton = ({
|
||||
const handleCancel = () => {
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
} else {
|
||||
} else if (setIsOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
@@ -53,6 +62,7 @@ const FormCancelButton = ({
|
||||
className="w-full bg-transparent"
|
||||
variant="faded"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
onPress={handleCancel}
|
||||
startContent={leftIcon}
|
||||
>
|
||||
@@ -65,6 +75,7 @@ const FormSubmitButton = ({
|
||||
children = "Save",
|
||||
loadingText = "Loading",
|
||||
isDisabled = false,
|
||||
color = "action",
|
||||
rightIcon,
|
||||
}: FormSubmitButtonProps) => {
|
||||
const { pending } = useFormStatus();
|
||||
@@ -75,8 +86,9 @@ const FormSubmitButton = ({
|
||||
ariaLabel="Save"
|
||||
className="w-full"
|
||||
variant="solid"
|
||||
color="action"
|
||||
color={color}
|
||||
size="lg"
|
||||
radius="lg"
|
||||
isLoading={pending}
|
||||
isDisabled={isDisabled}
|
||||
startContent={!pending && rightIcon}
|
||||
@@ -88,6 +100,7 @@ const FormSubmitButton = ({
|
||||
|
||||
export const FormButtons = ({
|
||||
setIsOpen,
|
||||
submitColor,
|
||||
onCancel,
|
||||
submitText = "Save",
|
||||
cancelText = "Cancel",
|
||||
@@ -110,6 +123,7 @@ export const FormButtons = ({
|
||||
loadingText={loadingText}
|
||||
isDisabled={isDisabled}
|
||||
rightIcon={rightIcon}
|
||||
color={submitColor}
|
||||
>
|
||||
{submitText}
|
||||
</FormSubmitButton>
|
||||
|
||||
@@ -69,11 +69,13 @@ const hideMenuItems = (menuGroups: GroupProps[], labelsToHide: string[]) => {
|
||||
export const Menu = ({ isOpen }: { isOpen: boolean }) => {
|
||||
const pathname = usePathname();
|
||||
const { permissions } = useAuth();
|
||||
const { hasProviders, openMutelistModal } = useUIStore();
|
||||
const { hasProviders, openMutelistModal, requestMutelistModalOpen } =
|
||||
useUIStore();
|
||||
const menuList = getMenuList({
|
||||
pathname,
|
||||
hasProviders,
|
||||
openMutelistModal,
|
||||
requestMutelistModalOpen,
|
||||
});
|
||||
|
||||
const labelsToHide = MENU_HIDE_RULES.filter((rule) =>
|
||||
|
||||
@@ -83,6 +83,7 @@ export const DeleteForm = ({
|
||||
variant="solid"
|
||||
color="danger"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <DeleteIcon size={24} />}
|
||||
>
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ApiKeySuccessModal = ({
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>⚠️ Important</AlertTitle>
|
||||
<AlertTitle>⚠️ Warning</AlertTitle>
|
||||
<AlertDescription>
|
||||
This is the only time you will see this API key. Please copy it now
|
||||
and store it securely. Once you close this dialog, the key cannot be
|
||||
|
||||
@@ -22,20 +22,20 @@ export const createApiKeyColumns = (
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="NAME" param="name" />
|
||||
<DataTableColumnHeader column={column} title="Name" param="name" />
|
||||
),
|
||||
cell: ({ row }) => <NameCell apiKey={row.original} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "prefix",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="PREFIX" param="prefix" />
|
||||
<DataTableColumnHeader column={column} title="Prefix" param="prefix" />
|
||||
),
|
||||
cell: ({ row }) => <PrefixCell apiKey={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
header: "EMAIL",
|
||||
header: "Email",
|
||||
cell: ({ row }) => <EmailCell apiKey={row.original} />,
|
||||
enableSorting: false,
|
||||
},
|
||||
@@ -44,7 +44,7 @@ export const createApiKeyColumns = (
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title="CREATED"
|
||||
title="Created"
|
||||
param="inserted_at"
|
||||
/>
|
||||
),
|
||||
@@ -52,7 +52,7 @@ export const createApiKeyColumns = (
|
||||
},
|
||||
{
|
||||
accessorKey: "last_used_at",
|
||||
header: "LAST USED",
|
||||
header: "Last Used",
|
||||
cell: ({ row }) => <LastUsedCell apiKey={row.original} />,
|
||||
enableSorting: false,
|
||||
},
|
||||
@@ -61,7 +61,7 @@ export const createApiKeyColumns = (
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title="EXPIRES"
|
||||
title="Expires"
|
||||
param="expires_at"
|
||||
/>
|
||||
),
|
||||
@@ -70,7 +70,7 @@ export const createApiKeyColumns = (
|
||||
{
|
||||
accessorKey: "revoked",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="STATUS" param="revoked" />
|
||||
<DataTableColumnHeader column={column} title="Status" param="revoked" />
|
||||
),
|
||||
cell: ({ row }) => <StatusCell apiKey={row.original} />,
|
||||
},
|
||||
|
||||
@@ -33,6 +33,9 @@ export function DataTableRowActions({
|
||||
onRevoke,
|
||||
}: DataTableRowActionsProps) {
|
||||
const apiKey = row.original;
|
||||
const isRevoked = apiKey.attributes.revoked;
|
||||
const isExpired = new Date(apiKey.attributes.expires_at) < new Date();
|
||||
const canRevoke = !isRevoked && !isExpired;
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center justify-end gap-2">
|
||||
@@ -62,23 +65,25 @@ export function DataTableRowActions({
|
||||
Edit name
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection title="Danger zone">
|
||||
<DropdownItem
|
||||
key="revoke"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
description="Revoke this API key permanently"
|
||||
textValue="Revoke"
|
||||
startContent={
|
||||
<DeleteDocumentBulkIcon
|
||||
className={clsx(iconClasses, "!text-danger")}
|
||||
/>
|
||||
}
|
||||
onPress={() => onRevoke(apiKey)}
|
||||
>
|
||||
Revoke
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
{canRevoke ? (
|
||||
<DropdownSection title="Danger zone">
|
||||
<DropdownItem
|
||||
key="revoke"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
description="Revoke this API key permanently"
|
||||
textValue="Revoke"
|
||||
startContent={
|
||||
<DeleteDocumentBulkIcon
|
||||
className={clsx(iconClasses, "!text-danger")}
|
||||
/>
|
||||
}
|
||||
onPress={() => onRevoke(apiKey)}
|
||||
>
|
||||
Revoke
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
) : null}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -8,10 +8,9 @@ import { createApiKey } from "@/actions/api-keys/api-keys";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { CustomAlertModal } from "@/components/ui/custom/custom-alert-modal";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Form, FormButtons } from "@/components/ui/form";
|
||||
|
||||
import { DEFAULT_EXPIRY_DAYS } from "./api-keys/constants";
|
||||
import { ModalButtons } from "./api-keys/modal-buttons";
|
||||
import { calculateExpiryDate } from "./api-keys/utils";
|
||||
|
||||
interface CreateApiKeyModalProps {
|
||||
@@ -45,8 +44,6 @@ export const CreateApiKeyModal = ({
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmitClient = async (values: FormValues) => {
|
||||
try {
|
||||
const result = await createApiKey({
|
||||
@@ -99,7 +96,7 @@ export const CreateApiKeyModal = ({
|
||||
onSubmit={form.handleSubmit(onSubmitClient)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex w-full justify-center gap-6">
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -127,12 +124,12 @@ export const CreateApiKeyModal = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ModalButtons
|
||||
<FormButtons
|
||||
onCancel={handleClose}
|
||||
onSubmit={form.handleSubmit(onSubmitClient)}
|
||||
isLoading={isLoading}
|
||||
isDisabled={!form.formState.isValid}
|
||||
submitText="Create API Key"
|
||||
cancelText="Cancel"
|
||||
loadingText="Processing..."
|
||||
isDisabled={!form.formState.isValid}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -9,9 +9,8 @@ import { updateApiKey } from "@/actions/api-keys/api-keys";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { CustomAlertModal } from "@/components/ui/custom/custom-alert-modal";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Form, FormButtons } from "@/components/ui/form";
|
||||
|
||||
import { ModalButtons } from "./api-keys/modal-buttons";
|
||||
import { EnrichedApiKey } from "./api-keys/types";
|
||||
import { isApiKeyNameDuplicate } from "./api-keys/utils";
|
||||
|
||||
@@ -45,8 +44,6 @@ export const EditApiKeyNameModal = ({
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
// Sync form data when apiKey changes or modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen && apiKey) {
|
||||
@@ -124,12 +121,12 @@ export const EditApiKeyNameModal = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ModalButtons
|
||||
<FormButtons
|
||||
onCancel={handleClose}
|
||||
onSubmit={form.handleSubmit(onSubmitClient)}
|
||||
isLoading={isLoading}
|
||||
isDisabled={!form.formState.isValid}
|
||||
submitText="Save Changes"
|
||||
cancelText="Cancel"
|
||||
loadingText="Processing..."
|
||||
isDisabled={!form.formState.isValid}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Snippet } from "@heroui/snippet";
|
||||
import { Trash2Icon } from "lucide-react";
|
||||
|
||||
import { revokeApiKey } from "@/actions/api-keys/api-keys";
|
||||
import {
|
||||
Alert,
|
||||
@@ -7,9 +10,9 @@ import {
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert/Alert";
|
||||
import { CustomAlertModal } from "@/components/ui/custom/custom-alert-modal";
|
||||
import { ModalButtons } from "@/components/ui/custom/custom-modal-buttons";
|
||||
|
||||
import { FALLBACK_VALUES } from "./api-keys/constants";
|
||||
import { ModalButtons } from "./api-keys/modal-buttons";
|
||||
import { EnrichedApiKey } from "./api-keys/types";
|
||||
import { useModalForm } from "./api-keys/use-modal-form";
|
||||
|
||||
@@ -54,23 +57,26 @@ export const RevokeApiKeyModal = ({
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle className="text-danger-700">⚠️ Warning</AlertTitle>
|
||||
<AlertDescription className="text-danger-600">
|
||||
<AlertTitle>⚠️ Warning</AlertTitle>
|
||||
<AlertDescription>
|
||||
This action cannot be undone. This API key will be revoked and will
|
||||
no longer work.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="text-sm">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>Are you sure you want to revoke this API key?</p>
|
||||
<div className="mt-2 rounded-lg bg-slate-800 p-3">
|
||||
<p className="font-medium text-white">
|
||||
{apiKey?.attributes.name || FALLBACK_VALUES.UNNAMED_KEY}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-slate-400">
|
||||
Prefix: {apiKey?.attributes.prefix}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Snippet
|
||||
hideSymbol
|
||||
hideCopyButton={true}
|
||||
classNames={{
|
||||
pre: "font-mono text-sm break-all whitespace-pre-wrap",
|
||||
}}
|
||||
>
|
||||
<p>{apiKey?.attributes.name || FALLBACK_VALUES.UNNAMED_KEY}</p>
|
||||
<p className="mt-1 text-xs">Prefix: {apiKey?.attributes.prefix}</p>
|
||||
</Snippet>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -87,6 +93,7 @@ export const RevokeApiKeyModal = ({
|
||||
isDisabled={!apiKey}
|
||||
submitText="Revoke API Key"
|
||||
submitColor="danger"
|
||||
submitIcon={<Trash2Icon size={24} />}
|
||||
/>
|
||||
</CustomAlertModal>
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Fira_Code as FontMono, Work_Sans as FontSans } from "next/font/google";
|
||||
import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google";
|
||||
|
||||
export const fontSans = FontSans({
|
||||
subsets: ["latin"],
|
||||
|
||||
@@ -40,8 +40,8 @@ export const useCredentialsForm = ({
|
||||
if (providerType === "gcp" && via === "service-account") {
|
||||
return addCredentialsServiceAccountFormSchema(providerType);
|
||||
}
|
||||
// For GitHub, we need to pass the via parameter to determine which fields are required
|
||||
if (providerType === "github") {
|
||||
// For GitHub and M365, we need to pass the via parameter to determine which fields are required
|
||||
if (providerType === "github" || providerType === "m365") {
|
||||
return addCredentialsFormSchema(providerType, via);
|
||||
}
|
||||
return addCredentialsFormSchema(providerType);
|
||||
@@ -99,13 +99,27 @@ export const useCredentialsForm = ({
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
};
|
||||
case "m365":
|
||||
// M365 credentials based on via parameter
|
||||
if (via === "app_client_secret") {
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.CLIENT_ID]: "",
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: "",
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
};
|
||||
}
|
||||
if (via === "app_certificate") {
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.CLIENT_ID]: "",
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: "",
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
};
|
||||
}
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.CLIENT_ID]: "",
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: "",
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
[ProviderCredentialFields.USER]: "",
|
||||
[ProviderCredentialFields.PASSWORD]: "",
|
||||
};
|
||||
case "gcp":
|
||||
return {
|
||||
@@ -146,9 +160,14 @@ export const useCredentialsForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const defaultValues = getDefaultValues();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: getDefaultValues(),
|
||||
defaultValues: defaultValues,
|
||||
mode: "onSubmit",
|
||||
reValidateMode: "onChange",
|
||||
criteriaMode: "all", // Show all errors for each field
|
||||
});
|
||||
|
||||
const { handleServerResponse } = useFormServerErrors(
|
||||
@@ -169,6 +188,7 @@ export const useCredentialsForm = ({
|
||||
|
||||
// Filter out empty values first, then append all remaining values
|
||||
const filteredValues = filterEmptyValues(values);
|
||||
|
||||
Object.entries(filteredValues).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
@@ -181,9 +201,12 @@ export const useCredentialsForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const { isSubmitting, errors } = form.formState;
|
||||
|
||||
return {
|
||||
form,
|
||||
isLoading: form.formState.isSubmitting,
|
||||
isLoading: isSubmitting,
|
||||
errors,
|
||||
handleSubmit,
|
||||
handleBackStep,
|
||||
searchParamsObj,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AWSWellArchitectedAttributesMetadata,
|
||||
Framework,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
@@ -68,9 +69,9 @@ export const mapComplianceData = (
|
||||
description: description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
pass: finalStatus === "PASS" ? 1 : 0,
|
||||
fail: finalStatus === "FAIL" ? 1 : 0,
|
||||
manual: finalStatus === "MANUAL" ? 1 : 0,
|
||||
pass: finalStatus === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: finalStatus === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: finalStatus === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
well_architected_name: attrs.Name,
|
||||
well_architected_question_id: attrs.WellArchitectedQuestionId,
|
||||
well_architected_practice_id: attrs.WellArchitectedPracticeId,
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content";
|
||||
import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title";
|
||||
import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
AttributesData,
|
||||
C5AttributesMetadata,
|
||||
Control,
|
||||
Framework,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
calculateFrameworkCounters,
|
||||
createRequirementsMap,
|
||||
findOrCreateCategory,
|
||||
findOrCreateControl,
|
||||
findOrCreateFramework,
|
||||
} from "./commons";
|
||||
|
||||
const getStatusCounters = (status: RequirementStatus) => ({
|
||||
pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
});
|
||||
|
||||
export const mapComplianceData = (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
): Framework[] => {
|
||||
const attributes = attributesData?.data || [];
|
||||
const requirementsMap = createRequirementsMap(requirementsData);
|
||||
const frameworks: Framework[] = [];
|
||||
|
||||
// Process attributes and merge with requirements data
|
||||
for (const attributeItem of attributes) {
|
||||
const id = attributeItem.id;
|
||||
const metadataArray = attributeItem.attributes?.attributes
|
||||
?.metadata as unknown as C5AttributesMetadata[];
|
||||
const attrs = metadataArray?.[0];
|
||||
if (!attrs) continue;
|
||||
|
||||
// Get corresponding requirement data
|
||||
const requirementData = requirementsMap.get(id);
|
||||
if (!requirementData) continue;
|
||||
|
||||
const frameworkName = attributeItem.attributes.framework;
|
||||
const categoryName = attrs.Section; // Level 1: Section (e.g., "Organisation of Information Security (OIS)")
|
||||
const controlLabel = attrs.SubSection; // Level 2: SubSection (e.g., "OIS-01 Information Security Management System (ISMS)")
|
||||
const description = attributeItem.attributes.description;
|
||||
const status = requirementData.attributes.status || "";
|
||||
const checks = attributeItem.attributes.attributes.check_ids || [];
|
||||
const requirementName = id;
|
||||
|
||||
// Find or create framework using common helper
|
||||
const framework = findOrCreateFramework(frameworks, frameworkName);
|
||||
|
||||
// Find or create category (Section) using common helper
|
||||
const category = findOrCreateCategory(framework.categories, categoryName);
|
||||
|
||||
// Find or create control (SubSection) using common helper
|
||||
const control = findOrCreateControl(category.controls, controlLabel);
|
||||
|
||||
// Create requirement
|
||||
const finalStatus: RequirementStatus = status as RequirementStatus;
|
||||
const requirement: Requirement = {
|
||||
name: requirementName,
|
||||
description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
...getStatusCounters(finalStatus),
|
||||
type: attrs.Type,
|
||||
about_criteria: attrs.AboutCriteria,
|
||||
complementary_criteria: attrs.ComplementaryCriteria,
|
||||
};
|
||||
|
||||
control.requirements.push(requirement);
|
||||
}
|
||||
|
||||
// Calculate counters using common helper
|
||||
calculateFrameworkCounters(frameworks);
|
||||
|
||||
return frameworks;
|
||||
};
|
||||
|
||||
const createRequirementItem = (
|
||||
requirement: Requirement,
|
||||
frameworkName: string,
|
||||
categoryName: string,
|
||||
controlIndex: number,
|
||||
reqIndex: number,
|
||||
scanId: string,
|
||||
): AccordionItemProps => ({
|
||||
key: `${frameworkName}-${categoryName}-control-${controlIndex}-req-${reqIndex}`,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type={requirement.type as string}
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
requirement={requirement}
|
||||
scanId={scanId}
|
||||
framework={frameworkName}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
});
|
||||
|
||||
const createControlItem = (
|
||||
control: Control,
|
||||
frameworkName: string,
|
||||
categoryName: string,
|
||||
controlIndex: number,
|
||||
scanId: string,
|
||||
): AccordionItemProps => ({
|
||||
key: `${frameworkName}-${categoryName}-control-${controlIndex}`,
|
||||
title: (
|
||||
<ComplianceAccordionTitle
|
||||
label={control.label}
|
||||
pass={control.pass}
|
||||
fail={control.fail}
|
||||
manual={control.manual}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
items: control.requirements.map((requirement, reqIndex) =>
|
||||
createRequirementItem(
|
||||
requirement,
|
||||
frameworkName,
|
||||
categoryName,
|
||||
controlIndex,
|
||||
reqIndex,
|
||||
scanId,
|
||||
),
|
||||
),
|
||||
isDisabled: control.pass === 0 && control.fail === 0 && control.manual === 0,
|
||||
});
|
||||
|
||||
export const toAccordionItems = (
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
): AccordionItemProps[] => {
|
||||
const safeId = scanId || "";
|
||||
|
||||
return data.flatMap((framework) =>
|
||||
framework.categories.map((category) => ({
|
||||
key: `${framework.name}-${category.name}`,
|
||||
title: (
|
||||
<ComplianceAccordionTitle
|
||||
label={category.name}
|
||||
pass={category.pass}
|
||||
fail={category.fail}
|
||||
manual={category.manual}
|
||||
isParentLevel={true}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
items: category.controls.map((control, controlIndex) =>
|
||||
createControlItem(
|
||||
control,
|
||||
framework.name,
|
||||
category.name,
|
||||
controlIndex,
|
||||
safeId,
|
||||
),
|
||||
),
|
||||
})),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content";
|
||||
import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title";
|
||||
import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
AttributesData,
|
||||
CCCAttributesMetadata,
|
||||
Framework,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
calculateFrameworkCounters,
|
||||
createRequirementsMap,
|
||||
findOrCreateCategory,
|
||||
findOrCreateControl,
|
||||
findOrCreateFramework,
|
||||
updateCounters,
|
||||
} from "./commons";
|
||||
|
||||
interface ProcessedItem {
|
||||
id: string;
|
||||
attrs: CCCAttributesMetadata;
|
||||
attributeItem: any;
|
||||
requirementData: any;
|
||||
}
|
||||
|
||||
// CCC-specific section configuration
|
||||
export interface CCCTextSection {
|
||||
title: string;
|
||||
key: keyof Requirement;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface CCCMappingSection {
|
||||
title: string;
|
||||
key: keyof Requirement;
|
||||
colorClasses: string;
|
||||
}
|
||||
|
||||
export const CCC_TEXT_SECTIONS: CCCTextSection[] = [
|
||||
{
|
||||
title: "Description",
|
||||
key: "description",
|
||||
},
|
||||
{
|
||||
title: "Family Description",
|
||||
key: "family_description",
|
||||
},
|
||||
{
|
||||
title: "SubSection",
|
||||
key: "subsection",
|
||||
},
|
||||
{
|
||||
title: "SubSection Objective",
|
||||
key: "subsection_objective",
|
||||
className: "whitespace-pre-wrap",
|
||||
},
|
||||
{
|
||||
title: "Recommendation",
|
||||
key: "recommendation",
|
||||
className: "whitespace-pre-wrap",
|
||||
},
|
||||
];
|
||||
|
||||
export const CCC_MAPPING_SECTIONS: CCCMappingSection[] = [
|
||||
{
|
||||
title: "Threat Mappings",
|
||||
key: "section_threat_mappings",
|
||||
colorClasses:
|
||||
"bg-red-50 text-red-700 ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20",
|
||||
},
|
||||
{
|
||||
title: "Guideline Mappings",
|
||||
key: "section_guideline_mappings",
|
||||
colorClasses:
|
||||
"bg-blue-50 text-blue-700 ring-blue-600/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20",
|
||||
},
|
||||
];
|
||||
|
||||
const createRequirement = (itemData: ProcessedItem): Requirement => {
|
||||
const { id, attrs, attributeItem, requirementData } = itemData;
|
||||
const description = attributeItem.attributes.description;
|
||||
const status = requirementData.attributes.status || "";
|
||||
const checks = attributeItem.attributes.attributes.check_ids || [];
|
||||
const finalStatus: RequirementStatus = status as RequirementStatus;
|
||||
|
||||
return {
|
||||
name: id,
|
||||
description: description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
pass: finalStatus === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: finalStatus === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: finalStatus === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
family_name: attrs.FamilyName,
|
||||
family_description: attrs.FamilyDescription,
|
||||
subsection: attrs.SubSection,
|
||||
subsection_objective: attrs.SubSectionObjective,
|
||||
applicability: attrs.Applicability,
|
||||
recommendation: attrs.Recommendation,
|
||||
section_threat_mappings: attrs.SectionThreatMappings,
|
||||
section_guideline_mappings: attrs.SectionGuidelineMappings,
|
||||
};
|
||||
};
|
||||
|
||||
export const mapComplianceData = (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
): Framework[] => {
|
||||
const attributes = attributesData?.data || [];
|
||||
const requirementsMap = createRequirementsMap(requirementsData);
|
||||
const frameworks: Framework[] = [];
|
||||
const itemsByFramework = new Map<string, ProcessedItem[]>();
|
||||
|
||||
// First pass: collect all data
|
||||
for (const attributeItem of attributes) {
|
||||
const id = attributeItem.id;
|
||||
const metadataArray = attributeItem.attributes?.attributes
|
||||
?.metadata as unknown as CCCAttributesMetadata[];
|
||||
const attrs = metadataArray?.[0];
|
||||
if (!attrs) continue;
|
||||
|
||||
const requirementData = requirementsMap.get(id);
|
||||
if (!requirementData) continue;
|
||||
|
||||
const frameworkName = attributeItem.attributes.framework;
|
||||
|
||||
if (!itemsByFramework.has(frameworkName)) {
|
||||
itemsByFramework.set(frameworkName, []);
|
||||
}
|
||||
|
||||
itemsByFramework.get(frameworkName)!.push({
|
||||
id,
|
||||
attrs,
|
||||
attributeItem,
|
||||
requirementData,
|
||||
});
|
||||
}
|
||||
|
||||
// Process each framework
|
||||
for (const [frameworkName, items] of Array.from(itemsByFramework.entries())) {
|
||||
const framework = findOrCreateFramework(frameworks, frameworkName);
|
||||
|
||||
// Group by FamilyName (Category) -> Section (Control) -> Requirements
|
||||
for (const itemData of items) {
|
||||
const requirement = createRequirement(itemData);
|
||||
const familyName = itemData.attrs.FamilyName;
|
||||
const sectionName = itemData.attrs.Section;
|
||||
|
||||
// Create 3-level hierarchy: FamilyName -> Section -> Requirements
|
||||
const category = findOrCreateCategory(framework.categories, familyName);
|
||||
const control = findOrCreateControl(category.controls, sectionName);
|
||||
|
||||
control.requirements.push(requirement);
|
||||
updateCounters(control, requirement.status);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate counters using common helper
|
||||
calculateFrameworkCounters(frameworks);
|
||||
|
||||
return frameworks;
|
||||
};
|
||||
|
||||
// Helper function to create accordion item for requirement
|
||||
const createRequirementAccordionItem = (
|
||||
requirement: Requirement,
|
||||
itemKey: string,
|
||||
scanId: string,
|
||||
frameworkName: string,
|
||||
): AccordionItemProps => ({
|
||||
key: itemKey,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
requirement={requirement}
|
||||
scanId={scanId}
|
||||
framework={frameworkName}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
});
|
||||
|
||||
export const toAccordionItems = (
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
): AccordionItemProps[] => {
|
||||
return data.flatMap((framework) =>
|
||||
framework.categories.map((category) => ({
|
||||
key: `${framework.name}-${category.name}`,
|
||||
title: (
|
||||
<ComplianceAccordionTitle
|
||||
label={category.name}
|
||||
pass={category.pass}
|
||||
fail={category.fail}
|
||||
manual={category.manual}
|
||||
isParentLevel={true}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
items: category.controls.map((control, i: number) => {
|
||||
const baseKey = `${framework.name}-${category.name}-control-${i}`;
|
||||
|
||||
return {
|
||||
key: baseKey,
|
||||
title: (
|
||||
<ComplianceAccordionTitle
|
||||
label={control.label}
|
||||
pass={control.pass}
|
||||
fail={control.fail}
|
||||
manual={control.manual}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
items: control.requirements.map((requirement, j: number) =>
|
||||
createRequirementAccordionItem(
|
||||
requirement,
|
||||
`${baseKey}-req-${j}`,
|
||||
scanId || "",
|
||||
framework.name,
|
||||
),
|
||||
),
|
||||
isDisabled:
|
||||
control.pass === 0 && control.fail === 0 && control.manual === 0,
|
||||
};
|
||||
}),
|
||||
})),
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CISAttributesMetadata,
|
||||
Framework,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
@@ -79,9 +80,9 @@ export const mapComplianceData = (
|
||||
description: attrs.Description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
pass: finalStatus === "PASS" ? 1 : 0,
|
||||
fail: finalStatus === "FAIL" ? 1 : 0,
|
||||
manual: finalStatus === "MANUAL" ? 1 : 0,
|
||||
pass: finalStatus === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: finalStatus === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: finalStatus === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
profile: attrs.Profile,
|
||||
subsection: attrs.SubSection || "",
|
||||
assessment_status: attrs.AssessmentStatus,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
FailedSection,
|
||||
Framework,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementItemData,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
@@ -12,11 +13,11 @@ export const updateCounters = (
|
||||
target: { pass: number; fail: number; manual: number },
|
||||
status: RequirementStatus,
|
||||
) => {
|
||||
if (status === "MANUAL") {
|
||||
if (status === REQUIREMENT_STATUS.MANUAL) {
|
||||
target.manual++;
|
||||
} else if (status === "PASS") {
|
||||
} else if (status === REQUIREMENT_STATUS.PASS) {
|
||||
target.pass++;
|
||||
} else if (status === "FAIL") {
|
||||
} else if (status === REQUIREMENT_STATUS.FAIL) {
|
||||
target.fail++;
|
||||
}
|
||||
};
|
||||
@@ -30,7 +31,7 @@ export const getTopFailedSections = (
|
||||
framework.categories.forEach((category) => {
|
||||
category.controls.forEach((control) => {
|
||||
control.requirements.forEach((requirement) => {
|
||||
if (requirement.status === "FAIL") {
|
||||
if (requirement.status === REQUIREMENT_STATUS.FAIL) {
|
||||
const sectionName = category.name;
|
||||
|
||||
if (!failedSectionMap.has(sectionName)) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from "react";
|
||||
|
||||
import { AWSWellArchitectedCustomDetails } from "@/components/compliance/compliance-custom-details/aws-well-architected-details";
|
||||
import { C5CustomDetails } from "@/components/compliance/compliance-custom-details/c5-details";
|
||||
import { CCCCustomDetails } from "@/components/compliance/compliance-custom-details/ccc-details";
|
||||
import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details";
|
||||
import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details";
|
||||
import { GenericCustomDetails } from "@/components/compliance/compliance-custom-details/generic-details";
|
||||
@@ -22,6 +24,14 @@ import {
|
||||
mapComplianceData as mapAWSWellArchitectedComplianceData,
|
||||
toAccordionItems as toAWSWellArchitectedAccordionItems,
|
||||
} from "./aws-well-architected";
|
||||
import {
|
||||
mapComplianceData as mapC5ComplianceData,
|
||||
toAccordionItems as toC5AccordionItems,
|
||||
} from "./c5";
|
||||
import {
|
||||
mapComplianceData as mapCCCComplianceData,
|
||||
toAccordionItems as toCCCAccordionItems,
|
||||
} from "./ccc";
|
||||
import {
|
||||
mapComplianceData as mapCISComplianceData,
|
||||
toAccordionItems as toCISAccordionItems,
|
||||
@@ -80,6 +90,15 @@ const getDefaultMapper = (): ComplianceMapper => ({
|
||||
});
|
||||
|
||||
const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
C5: {
|
||||
mapComplianceData: mapC5ComplianceData,
|
||||
toAccordionItems: toC5AccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(C5CustomDetails, { requirement }),
|
||||
},
|
||||
ENS: {
|
||||
mapComplianceData: mapENSComplianceData,
|
||||
toAccordionItems: toENSAccordionItems,
|
||||
@@ -151,6 +170,15 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(ThreatCustomDetails, { requirement }),
|
||||
},
|
||||
CCC: {
|
||||
mapComplianceData: mapCCCComplianceData,
|
||||
toAccordionItems: toCCCAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(CCCCustomDetails, { requirement }),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ENSAttributesMetadata,
|
||||
Framework,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
@@ -82,7 +83,7 @@ export const mapComplianceData = (
|
||||
|
||||
// Create requirement
|
||||
const finalStatus: RequirementStatus = isManual
|
||||
? "MANUAL"
|
||||
? REQUIREMENT_STATUS.MANUAL
|
||||
: (status as RequirementStatus);
|
||||
const requirement: Requirement = {
|
||||
name: requirementName,
|
||||
@@ -90,9 +91,9 @@ export const mapComplianceData = (
|
||||
status: finalStatus,
|
||||
type,
|
||||
check_ids: checks,
|
||||
pass: finalStatus === "PASS" ? 1 : 0,
|
||||
fail: finalStatus === "FAIL" ? 1 : 0,
|
||||
manual: finalStatus === "MANUAL" ? 1 : 0,
|
||||
pass: finalStatus === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: finalStatus === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: finalStatus === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
nivel: attrs.Nivel || "",
|
||||
dimensiones: attrs.Dimensiones || [],
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Framework,
|
||||
GenericAttributesMetadata,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
@@ -41,9 +42,9 @@ const createRequirement = (itemData: ProcessedItem): Requirement => {
|
||||
description: description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
pass: finalStatus === "PASS" ? 1 : 0,
|
||||
fail: finalStatus === "FAIL" ? 1 : 0,
|
||||
manual: finalStatus === "MANUAL" ? 1 : 0,
|
||||
pass: finalStatus === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: finalStatus === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: finalStatus === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
item_id: attrs.ItemId,
|
||||
subsection: attrs.SubSection,
|
||||
subgroup: attrs.SubGroup || undefined,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Framework,
|
||||
ISO27001AttributesMetadata,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
@@ -66,9 +67,9 @@ export const mapComplianceData = (
|
||||
description: description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
pass: finalStatus === "PASS" ? 1 : 0,
|
||||
fail: finalStatus === "FAIL" ? 1 : 0,
|
||||
manual: finalStatus === "MANUAL" ? 1 : 0,
|
||||
pass: finalStatus === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: finalStatus === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: finalStatus === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
objetive_name: objetiveName,
|
||||
check_summary: checkSummary,
|
||||
control_label: controlLabel,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Framework,
|
||||
KISAAttributesMetadata,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
@@ -65,9 +66,9 @@ export const mapComplianceData = (
|
||||
description: description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
pass: finalStatus === "PASS" ? 1 : 0,
|
||||
fail: finalStatus === "FAIL" ? 1 : 0,
|
||||
manual: finalStatus === "MANUAL" ? 1 : 0,
|
||||
pass: finalStatus === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: finalStatus === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: finalStatus === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
section: sectionName,
|
||||
audit_checklist: attrs.AuditChecklist,
|
||||
related_regulations: attrs.RelatedRegulations,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Framework,
|
||||
MITREAttributesMetadata,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
@@ -62,9 +63,9 @@ export const mapComplianceData = (
|
||||
description: description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
pass: finalStatus === "PASS" ? 1 : 0,
|
||||
fail: finalStatus === "FAIL" ? 1 : 0,
|
||||
manual: finalStatus === "MANUAL" ? 1 : 0,
|
||||
pass: finalStatus === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: finalStatus === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: finalStatus === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
// MITRE specific fields
|
||||
technique_id: id,
|
||||
technique_name: techniqueName,
|
||||
@@ -154,7 +155,7 @@ export const getTopFailedSections = (
|
||||
const requirements = (framework as any).requirements || [];
|
||||
|
||||
requirements.forEach((requirement: Requirement) => {
|
||||
if (requirement.status === "FAIL") {
|
||||
if (requirement.status === REQUIREMENT_STATUS.FAIL) {
|
||||
const tactics = (requirement.tactics as string[]) || [];
|
||||
|
||||
tactics.forEach((tactic) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AttributesData,
|
||||
Framework,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
ThreatAttributesMetadata,
|
||||
@@ -56,7 +57,7 @@ export const mapComplianceData = (
|
||||
const totalFindings = requirementData.attributes.total_findings || 0;
|
||||
|
||||
// Calculate score: if PASS = levelOfRisk * weight, if FAIL = 0
|
||||
const score = status === "PASS" ? levelOfRisk * weight : 0;
|
||||
const score = status === REQUIREMENT_STATUS.PASS ? levelOfRisk * weight : 0;
|
||||
|
||||
// Find or create framework using common helper
|
||||
const framework = findOrCreateFramework(frameworks, frameworkName);
|
||||
@@ -74,9 +75,9 @@ export const mapComplianceData = (
|
||||
description: description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
pass: finalStatus === "PASS" ? 1 : 0,
|
||||
fail: finalStatus === "FAIL" ? 1 : 0,
|
||||
manual: finalStatus === "MANUAL" ? 1 : 0,
|
||||
pass: finalStatus === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: finalStatus === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: finalStatus === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
title: title,
|
||||
levelOfRisk: levelOfRisk,
|
||||
weight: weight,
|
||||
|
||||
+22
-3
@@ -18,6 +18,7 @@ import {
|
||||
VolumeX,
|
||||
Warehouse,
|
||||
} from "lucide-react";
|
||||
import type { MouseEvent } from "react";
|
||||
|
||||
import { ProwlerShort } from "@/components/icons";
|
||||
import {
|
||||
@@ -39,12 +40,14 @@ interface MenuListOptions {
|
||||
pathname: string;
|
||||
hasProviders?: boolean;
|
||||
openMutelistModal?: () => void;
|
||||
requestMutelistModalOpen?: () => void;
|
||||
}
|
||||
|
||||
export const getMenuList = ({
|
||||
pathname,
|
||||
hasProviders,
|
||||
openMutelistModal,
|
||||
requestMutelistModalOpen,
|
||||
}: MenuListOptions): GroupProps[] => {
|
||||
return [
|
||||
{
|
||||
@@ -164,12 +167,28 @@ export const getMenuList = ({
|
||||
submenus: [
|
||||
{ href: "/providers", label: "Cloud Providers", icon: CloudCog },
|
||||
{
|
||||
// Use trailing slash to prevent both menu items from being active at /providers
|
||||
href: "/providers/",
|
||||
href: "/providers",
|
||||
label: "Mutelist",
|
||||
icon: VolumeX,
|
||||
disabled: hasProviders === false,
|
||||
onClick: openMutelistModal,
|
||||
active: false,
|
||||
onClick: (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
if (hasProviders === false) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
requestMutelistModalOpen?.();
|
||||
|
||||
if (pathname !== "/providers") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openMutelistModal?.();
|
||||
},
|
||||
},
|
||||
{ href: "/manage-groups", label: "Provider Groups", icon: Group },
|
||||
{ href: "/scans", label: "Scan Jobs", icon: Timer },
|
||||
|
||||
@@ -80,14 +80,21 @@ export const buildAzureSecret = (formData: FormData) => {
|
||||
|
||||
export const buildM365Secret = (formData: FormData) => {
|
||||
const secret = {
|
||||
...buildAzureSecret(formData),
|
||||
[ProviderCredentialFields.USER]: getFormValue(
|
||||
[ProviderCredentialFields.CLIENT_ID]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.USER,
|
||||
ProviderCredentialFields.CLIENT_ID,
|
||||
),
|
||||
[ProviderCredentialFields.PASSWORD]: getFormValue(
|
||||
[ProviderCredentialFields.TENANT_ID]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.PASSWORD,
|
||||
ProviderCredentialFields.TENANT_ID,
|
||||
),
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.CLIENT_SECRET,
|
||||
),
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.CERTIFICATE_CONTENT,
|
||||
),
|
||||
};
|
||||
return filterEmptyValues(secret);
|
||||
|
||||
@@ -29,6 +29,7 @@ export const ProviderCredentialFields = {
|
||||
TENANT_ID: "tenant_id",
|
||||
USER: "user",
|
||||
PASSWORD: "password",
|
||||
CERTIFICATE_CONTENT: "certificate_content",
|
||||
|
||||
// GCP fields
|
||||
REFRESH_TOKEN: "refresh_token",
|
||||
@@ -70,6 +71,7 @@ export const ErrorPointers = {
|
||||
OAUTH_APP_TOKEN: "/data/attributes/secret/oauth_app_token",
|
||||
GITHUB_APP_ID: "/data/attributes/secret/github_app_id",
|
||||
GITHUB_APP_KEY: "/data/attributes/secret/github_app_key_content",
|
||||
CERTIFICATE_CONTENT: "/data/attributes/secret/certificate_content",
|
||||
} as const;
|
||||
|
||||
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];
|
||||
@@ -53,7 +53,7 @@ export const getProviderFormType = (
|
||||
via?: string,
|
||||
): ProviderFormType => {
|
||||
// Providers that need credential type selection
|
||||
const needsSelector = ["aws", "gcp", "github"].includes(providerType);
|
||||
const needsSelector = ["aws", "gcp", "github", "m365"].includes(providerType);
|
||||
|
||||
// Show selector if no via parameter and provider needs it
|
||||
if (needsSelector && !via) {
|
||||
@@ -80,6 +80,14 @@ export const getProviderFormType = (
|
||||
return "credentials";
|
||||
}
|
||||
|
||||
// M365 credential types
|
||||
if (
|
||||
providerType === "m365" &&
|
||||
["app_client_secret", "app_certificate"].includes(via || "")
|
||||
) {
|
||||
return "credentials";
|
||||
}
|
||||
|
||||
// Other providers go directly to credentials form
|
||||
if (!needsSelector) {
|
||||
return "credentials";
|
||||
@@ -99,6 +107,8 @@ export const requiresBackButton = (via?: string | null): boolean => {
|
||||
"personal_access_token",
|
||||
"oauth_app",
|
||||
"github_app",
|
||||
"app_client_secret",
|
||||
"app_certificate",
|
||||
];
|
||||
|
||||
return validViaTypes.includes(via);
|
||||
|
||||
@@ -74,6 +74,14 @@ export default defineConfig({
|
||||
name: "invite-and-manage-users.auth.setup",
|
||||
testMatch: "invite-and-manage-users.auth.setup.ts",
|
||||
},
|
||||
|
||||
// All authentication setups combined
|
||||
// Runs all authentication setup files to create all user states
|
||||
{
|
||||
name: "all.auth.setup",
|
||||
testMatch: "**/*.auth.setup.ts",
|
||||
},
|
||||
|
||||
// ===========================================
|
||||
// Test Suite Projects
|
||||
// ===========================================
|
||||
|
||||
+11
-1
@@ -5,12 +5,15 @@ interface UIStoreState {
|
||||
isSideMenuOpen: boolean;
|
||||
isMutelistModalOpen: boolean;
|
||||
hasProviders: boolean;
|
||||
shouldAutoOpenMutelist: boolean;
|
||||
|
||||
openSideMenu: () => void;
|
||||
closeSideMenu: () => void;
|
||||
openMutelistModal: () => void;
|
||||
closeMutelistModal: () => void;
|
||||
setHasProviders: (value: boolean) => void;
|
||||
requestMutelistModalOpen: () => void;
|
||||
resetMutelistModalRequest: () => void;
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIStoreState>()(
|
||||
@@ -19,11 +22,18 @@ export const useUIStore = create<UIStoreState>()(
|
||||
isSideMenuOpen: false,
|
||||
isMutelistModalOpen: false,
|
||||
hasProviders: false,
|
||||
shouldAutoOpenMutelist: false,
|
||||
openSideMenu: () => set({ isSideMenuOpen: true }),
|
||||
closeSideMenu: () => set({ isSideMenuOpen: false }),
|
||||
openMutelistModal: () => set({ isMutelistModalOpen: true }),
|
||||
openMutelistModal: () =>
|
||||
set({
|
||||
isMutelistModalOpen: true,
|
||||
shouldAutoOpenMutelist: false,
|
||||
}),
|
||||
closeMutelistModal: () => set({ isMutelistModalOpen: false }),
|
||||
setHasProviders: (value: boolean) => set({ hasProviders: value }),
|
||||
requestMutelistModalOpen: () => set({ shouldAutoOpenMutelist: true }),
|
||||
resetMutelistModalRequest: () => set({ shouldAutoOpenMutelist: false }),
|
||||
}),
|
||||
{
|
||||
name: "ui-store",
|
||||
|
||||
@@ -1,241 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
@theme {
|
||||
/* Font Families are injected by Next.js from config/fonts.ts */
|
||||
/* --font-sans: Work Sans (from Next.js) */
|
||||
/* --font-mono: Fira Code (from Next.js) */
|
||||
|
||||
/* Text Sizes with Line Heights - Exact from Figma Design System */
|
||||
--text-xs: 0.625rem; /* 10px */
|
||||
--text-xs--line-height: 1.6;
|
||||
--text-sm: 0.75rem; /* 12px */
|
||||
--text-sm--line-height: 1.667;
|
||||
--text-base: 0.875rem; /* 14px */
|
||||
--text-base--line-height: 1.714;
|
||||
--text-lg: 1rem; /* 16px */
|
||||
--text-lg--line-height: 1.75;
|
||||
--text-xl: 1.125rem; /* 18px */
|
||||
--text-xl--line-height: 1.556;
|
||||
--text-2xl: 1.25rem; /* 20px */
|
||||
--text-2xl--line-height: 1.6;
|
||||
--text-3xl: 1.5rem; /* 24px */
|
||||
--text-3xl--line-height: 1.5;
|
||||
--text-4xl: 1.875rem; /* 30px */
|
||||
--text-4xl--line-height: 1.333;
|
||||
--text-5xl: 2.25rem; /* 36px */
|
||||
--text-5xl--line-height: 1.333;
|
||||
--text-6xl: 3rem; /* 48px */
|
||||
--text-6xl--line-height: 1.25;
|
||||
--text-7xl: 3.75rem; /* 60px */
|
||||
--text-7xl--line-height: 1.2;
|
||||
--text-8xl: 4.5rem; /* 72px */
|
||||
--text-8xl--line-height: 1.333;
|
||||
--text-9xl: 6rem; /* 96px */
|
||||
--text-9xl--line-height: 1.333;
|
||||
|
||||
/* Font Weights - Complete Inter Scale */
|
||||
--font-weight-thin: 100;
|
||||
--font-weight-extralight: 200;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-extrabold: 800;
|
||||
--font-weight-black: 900;
|
||||
|
||||
/* Info & Primary Colors */
|
||||
--color-info: #2e51b2;
|
||||
--color-primary: #2e51b2;
|
||||
|
||||
/* Warning Colors */
|
||||
--color-warning: #fdd34f;
|
||||
--color-warning-emphasis: #ff7d19;
|
||||
|
||||
/* Danger & Destructive Colors */
|
||||
--color-danger: #ff3077;
|
||||
--color-danger-emphasis: #971348;
|
||||
--color-destructive: #db2b49;
|
||||
|
||||
/* Success Colors */
|
||||
--color-success: #86da26;
|
||||
--color-success-emphasis: #20b853;
|
||||
|
||||
/* Provider-specific Colors */
|
||||
--color-orange: #ff9800;
|
||||
--color-cyan: #06b6d4;
|
||||
--color-red: #ef4444;
|
||||
|
||||
/* Accent Colors */
|
||||
--color-magenta: #b51c80;
|
||||
--color-purple-dark: #5f1551;
|
||||
|
||||
/* Chart Card Colors - From Figma */
|
||||
--color-card-bg: #080f23;
|
||||
--color-card-border: #171d30;
|
||||
--color-text-accent: #fface2;
|
||||
--color-page-bg: #1a2034;
|
||||
|
||||
/* Alert Pill Colors */
|
||||
--color-alert-pill-bg: #432232;
|
||||
--color-alert-pill-text: #f54280;
|
||||
|
||||
/* ===========================
|
||||
Border Radius
|
||||
=========================== */
|
||||
|
||||
/* Widget Border Radius - From Figma */
|
||||
--radius-widget: 12px;
|
||||
|
||||
/* White & Black */
|
||||
--color-white: #ffffff;
|
||||
--color-black: #000000;
|
||||
|
||||
/* Slate Scale */
|
||||
--color-slate-50: #f8fafc;
|
||||
--color-slate-100: #f1f5f9;
|
||||
--color-slate-200: #e2e8f0;
|
||||
--color-slate-300: #cbd5e1;
|
||||
--color-slate-400: #94a3b8;
|
||||
--color-slate-500: #64748b;
|
||||
--color-slate-600: #475569;
|
||||
--color-slate-700: #334155;
|
||||
--color-slate-800: #1e293b;
|
||||
--color-slate-900: #0f172a;
|
||||
--color-slate-950: #020617;
|
||||
|
||||
/* Gray Scale */
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-300: #d1d5db;
|
||||
--color-gray-400: #9ca3af;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
--color-gray-950: #030712;
|
||||
|
||||
/* Zinc Scale */
|
||||
--color-zinc-50: #fafafa;
|
||||
--color-zinc-100: #f4f4f5;
|
||||
--color-zinc-200: #e4e4e7;
|
||||
--color-zinc-300: #d4d4d8;
|
||||
--color-zinc-400: #a1a1aa;
|
||||
--color-zinc-500: #71717a;
|
||||
--color-zinc-600: #52525b;
|
||||
--color-zinc-700: #3f3f46;
|
||||
--color-zinc-800: #27272a;
|
||||
--color-zinc-900: #18181b;
|
||||
--color-zinc-950: #09090b;
|
||||
|
||||
/* Stone Scale */
|
||||
--color-stone-50: #fafaf9;
|
||||
--color-stone-100: #f5f5f4;
|
||||
--color-stone-200: #e7e5e4;
|
||||
--color-stone-300: #d6d3d1;
|
||||
--color-stone-400: #a8a29e;
|
||||
--color-stone-500: #78716c;
|
||||
--color-stone-600: #57534e;
|
||||
--color-stone-700: #44403c;
|
||||
--color-stone-800: #292524;
|
||||
--color-stone-900: #1c1917;
|
||||
--color-stone-950: #0c0a09;
|
||||
|
||||
/* Red Scale */
|
||||
--color-red-50: #fef2f2;
|
||||
--color-red-100: #fee2e2;
|
||||
--color-red-200: #fecaca;
|
||||
--color-red-300: #fca5a5;
|
||||
--color-red-400: #f87171;
|
||||
--color-red-500: #ef4444;
|
||||
--color-red-600: #dc2626;
|
||||
--color-red-700: #b91c1c;
|
||||
--color-red-800: #991b1b;
|
||||
--color-red-900: #7f1d1d;
|
||||
--color-red-950: #450a0a;
|
||||
|
||||
/* Rose Scale */
|
||||
--color-rose-50: #fff1f2;
|
||||
--color-rose-100: #ffe4e6;
|
||||
--color-rose-200: #fecdd3;
|
||||
--color-rose-300: #fda4af;
|
||||
--color-rose-400: #fb7185;
|
||||
--color-rose-500: #f43f5e;
|
||||
--color-rose-600: #e11d48;
|
||||
--color-rose-700: #be123c;
|
||||
--color-rose-800: #9f1239;
|
||||
--color-rose-900: #881337;
|
||||
--color-rose-950: #4c0519;
|
||||
|
||||
/* Pink Scale */
|
||||
--color-pink-50: #fdf2f8;
|
||||
--color-pink-100: #fce7f3;
|
||||
--color-pink-200: #fbcfe8;
|
||||
--color-pink-300: #f9a8d4;
|
||||
--color-pink-400: #f472b6;
|
||||
--color-pink-500: #ec4899;
|
||||
--color-pink-600: #db2777;
|
||||
--color-pink-700: #be185d;
|
||||
--color-pink-800: #9d174d;
|
||||
--color-pink-900: #831843;
|
||||
--color-pink-950: #500724;
|
||||
|
||||
/* Fuchsia Scale */
|
||||
--color-fuchsia-50: #fdf4ff;
|
||||
--color-fuchsia-100: #fae8ff;
|
||||
--color-fuchsia-200: #f5d0fe;
|
||||
--color-fuchsia-300: #f0abfc;
|
||||
--color-fuchsia-400: #e879f9;
|
||||
--color-fuchsia-500: #d946ef;
|
||||
--color-fuchsia-600: #c026d3;
|
||||
--color-fuchsia-700: #a21caf;
|
||||
--color-fuchsia-800: #86198f;
|
||||
--color-fuchsia-900: #701a75;
|
||||
--color-fuchsia-950: #4a044e;
|
||||
|
||||
/* Purple Scale */
|
||||
--color-purple-50: #faf5ff;
|
||||
--color-purple-100: #f3e8ff;
|
||||
--color-purple-200: #e9d5ff;
|
||||
--color-purple-300: #d8b4fe;
|
||||
--color-purple-400: #c084fc;
|
||||
--color-purple-500: #a855f7;
|
||||
--color-purple-600: #9333ea;
|
||||
--color-purple-700: #7e22ce;
|
||||
--color-purple-800: #6b21a8;
|
||||
--color-purple-900: #581c87;
|
||||
--color-purple-950: #3b0764;
|
||||
|
||||
/* Violet Scale */
|
||||
--color-violet-50: #f5f3ff;
|
||||
--color-violet-100: #ede9fe;
|
||||
--color-violet-200: #ddd6fe;
|
||||
--color-violet-300: #c4b5fd;
|
||||
--color-violet-400: #a78bfa;
|
||||
--color-violet-500: #8b5cf6;
|
||||
--color-violet-600: #7c3aed;
|
||||
--color-violet-700: #6d28d9;
|
||||
--color-violet-800: #5b21b6;
|
||||
--color-violet-900: #4c1d95;
|
||||
--color-violet-950: #2e1065;
|
||||
|
||||
/* Indigo Scale */
|
||||
--color-indigo-50: #eef2ff;
|
||||
--color-indigo-100: #e0e7ff;
|
||||
--color-indigo-200: #c7d2fe;
|
||||
--color-indigo-300: #a5b4fc;
|
||||
--color-indigo-400: #818cf8;
|
||||
--color-indigo-500: #6366f1;
|
||||
--color-indigo-600: #4f46e5;
|
||||
--color-indigo-700: #4338ca;
|
||||
--color-indigo-800: #3730a3;
|
||||
--color-indigo-900: #312e81;
|
||||
--color-indigo-950: #1e1b4b;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--chart-success: 146 80% 35%;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Page, Locator, expect } from "@playwright/test";
|
||||
|
||||
export class HomePage {
|
||||
readonly page: Page;
|
||||
|
||||
// Main content elements
|
||||
readonly mainContent: Locator;
|
||||
readonly breadcrumbs: Locator;
|
||||
readonly overviewHeading: Locator;
|
||||
|
||||
// Navigation elements
|
||||
readonly navigationMenu: Locator;
|
||||
readonly userMenu: Locator;
|
||||
readonly signOutButton: Locator;
|
||||
|
||||
// Dashboard elements
|
||||
readonly dashboardCards: Locator;
|
||||
readonly overviewSection: Locator;
|
||||
|
||||
// UI elements
|
||||
readonly themeToggle: Locator;
|
||||
readonly logo: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
// Main content elements
|
||||
this.mainContent = page.locator("main");
|
||||
this.breadcrumbs = page.getByLabel("Breadcrumbs");
|
||||
this.overviewHeading = page.getByRole("heading", { name: "Overview", exact: true });
|
||||
|
||||
// Navigation elements
|
||||
this.navigationMenu = page.locator("nav");
|
||||
this.userMenu = page.getByRole("button", { name: /user menu/i });
|
||||
this.signOutButton = page.getByRole("button", { name: "Sign out" });
|
||||
|
||||
// Dashboard elements
|
||||
this.dashboardCards = page.locator('[data-testid="dashboard-card"]');
|
||||
this.overviewSection = page.locator('[data-testid="overview-section"]');
|
||||
|
||||
// UI elements
|
||||
this.themeToggle = page.getByLabel("Toggle theme");
|
||||
this.logo = page.locator('svg[width="300"]');
|
||||
}
|
||||
|
||||
// Navigation methods
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto("/");
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async waitForPageLoad(): Promise<void> {
|
||||
await this.page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
// Verification methods
|
||||
async verifyPageLoaded(): Promise<void> {
|
||||
await expect(this.page).toHaveURL("/");
|
||||
await expect(this.mainContent).toBeVisible();
|
||||
await expect(this.overviewHeading).toBeVisible();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async verifyBreadcrumbs(): Promise<void> {
|
||||
await expect(this.breadcrumbs).toBeVisible();
|
||||
await expect(this.overviewHeading).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyMainContent(): Promise<void> {
|
||||
await expect(this.mainContent).toBeVisible();
|
||||
}
|
||||
|
||||
// Navigation methods
|
||||
async navigateToOverview(): Promise<void> {
|
||||
await this.overviewHeading.click();
|
||||
}
|
||||
|
||||
async openUserMenu(): Promise<void> {
|
||||
await this.userMenu.click();
|
||||
}
|
||||
|
||||
async signOut(): Promise<void> {
|
||||
await this.openUserMenu();
|
||||
await this.signOutButton.click();
|
||||
}
|
||||
|
||||
// Dashboard methods
|
||||
async verifyDashboardCards(): Promise<void> {
|
||||
await expect(this.dashboardCards.first()).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyOverviewSection(): Promise<void> {
|
||||
await expect(this.overviewSection).toBeVisible();
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
async refresh(): Promise<void> {
|
||||
await this.page.reload();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async goBack(): Promise<void> {
|
||||
await this.page.goBack();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
// Accessibility methods
|
||||
async verifyKeyboardNavigation(): Promise<void> {
|
||||
// Test tab navigation through main elements
|
||||
await this.page.keyboard.press("Tab");
|
||||
await expect(this.themeToggle).toBeFocused();
|
||||
}
|
||||
|
||||
// Wait methods
|
||||
async waitForRedirect(expectedUrl: string): Promise<void> {
|
||||
await this.page.waitForURL(expectedUrl);
|
||||
}
|
||||
|
||||
async waitForContentLoad(): Promise<void> {
|
||||
await this.page.waitForFunction(() => {
|
||||
const main = document.querySelector("main");
|
||||
return main && main.offsetHeight > 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import { Page, Locator, expect } from "@playwright/test";
|
||||
import { HomePage } from "./home-page";
|
||||
|
||||
export interface SignInCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface SocialAuthConfig {
|
||||
googleEnabled: boolean;
|
||||
githubEnabled: boolean;
|
||||
}
|
||||
|
||||
export class SignInPage {
|
||||
readonly page: Page;
|
||||
readonly homePage: HomePage;
|
||||
|
||||
// Form elements
|
||||
readonly emailInput: Locator;
|
||||
readonly passwordInput: Locator;
|
||||
readonly loginButton: Locator;
|
||||
readonly form: Locator;
|
||||
|
||||
// Social authentication buttons
|
||||
readonly googleButton: Locator;
|
||||
readonly githubButton: Locator;
|
||||
readonly samlButton: Locator;
|
||||
|
||||
// Navigation elements
|
||||
readonly signUpLink: Locator;
|
||||
readonly backButton: Locator;
|
||||
|
||||
// UI elements
|
||||
readonly title: Locator;
|
||||
readonly logo: Locator;
|
||||
readonly themeToggle: Locator;
|
||||
|
||||
// Error messages
|
||||
readonly errorMessages: Locator;
|
||||
readonly loadingIndicator: Locator;
|
||||
|
||||
// SAML specific elements
|
||||
readonly samlModeTitle: Locator;
|
||||
readonly samlEmailInput: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.homePage = new HomePage(page);
|
||||
|
||||
// Form elements
|
||||
this.emailInput = page.getByLabel("Email");
|
||||
this.passwordInput = page.getByLabel("Password");
|
||||
this.loginButton = page.getByRole("button", { name: "Log in" });
|
||||
this.form = page.locator("form");
|
||||
|
||||
// Social authentication buttons
|
||||
this.googleButton = page.getByText("Continue with Google");
|
||||
this.githubButton = page.getByText("Continue with Github");
|
||||
this.samlButton = page.getByText("Continue with SAML SSO");
|
||||
|
||||
// Navigation elements
|
||||
this.signUpLink = page.getByRole("link", { name: "Sign up" });
|
||||
this.backButton = page.getByText("Back");
|
||||
|
||||
// UI elements
|
||||
this.title = page.getByText("Sign in", { exact: true });
|
||||
this.logo = page.locator('svg[width="300"]');
|
||||
this.themeToggle = page.getByLabel("Toggle theme");
|
||||
|
||||
// Error messages
|
||||
this.errorMessages = page.locator('[role="alert"], .error-message, [data-testid="error"]');
|
||||
this.loadingIndicator = page.getByText("Loading");
|
||||
|
||||
// SAML specific elements
|
||||
this.samlModeTitle = page.getByText("Sign in with SAML SSO");
|
||||
this.samlEmailInput = page.getByLabel("Email");
|
||||
}
|
||||
|
||||
// Navigation methods
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto("/sign-in");
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async waitForPageLoad(): Promise<void> {
|
||||
await this.page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
// Form interaction methods
|
||||
async fillEmail(email: string): Promise<void> {
|
||||
await this.emailInput.fill(email);
|
||||
}
|
||||
|
||||
async fillPassword(password: string): Promise<void> {
|
||||
await this.passwordInput.fill(password);
|
||||
}
|
||||
|
||||
async fillCredentials(credentials: SignInCredentials): Promise<void> {
|
||||
await this.fillEmail(credentials.email);
|
||||
await this.fillPassword(credentials.password);
|
||||
}
|
||||
|
||||
async submitForm(): Promise<void> {
|
||||
await this.loginButton.click();
|
||||
}
|
||||
|
||||
async login(credentials: SignInCredentials): Promise<void> {
|
||||
await this.fillCredentials(credentials);
|
||||
await this.submitForm();
|
||||
}
|
||||
|
||||
// Social authentication methods
|
||||
async clickGoogleAuth(): Promise<void> {
|
||||
await this.googleButton.click();
|
||||
}
|
||||
|
||||
async clickGithubAuth(): Promise<void> {
|
||||
await this.githubButton.click();
|
||||
}
|
||||
|
||||
async clickSamlAuth(): Promise<void> {
|
||||
await this.samlButton.click();
|
||||
}
|
||||
|
||||
// SAML SSO methods
|
||||
async toggleSamlMode(): Promise<void> {
|
||||
await this.clickSamlAuth();
|
||||
}
|
||||
|
||||
async goBackFromSaml(): Promise<void> {
|
||||
await this.backButton.click();
|
||||
}
|
||||
|
||||
async fillSamlEmail(email: string): Promise<void> {
|
||||
await this.samlEmailInput.fill(email);
|
||||
}
|
||||
|
||||
async submitSamlForm(): Promise<void> {
|
||||
await this.submitForm();
|
||||
}
|
||||
|
||||
// Navigation methods
|
||||
async goToSignUp(): Promise<void> {
|
||||
await this.signUpLink.click();
|
||||
}
|
||||
|
||||
// Validation and assertion methods
|
||||
async verifyPageLoaded(): Promise<void> {
|
||||
await expect(this.page).toHaveTitle(/Prowler/);
|
||||
await expect(this.logo).toBeVisible();
|
||||
await expect(this.title).toBeVisible();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async verifyFormElements(): Promise<void> {
|
||||
await expect(this.emailInput).toBeVisible();
|
||||
await expect(this.passwordInput).toBeVisible();
|
||||
await expect(this.loginButton).toBeVisible();
|
||||
}
|
||||
|
||||
async verifySocialButtons(config: SocialAuthConfig): Promise<void> {
|
||||
if (config.googleEnabled) {
|
||||
await expect(this.googleButton).toBeVisible();
|
||||
}
|
||||
if (config.githubEnabled) {
|
||||
await expect(this.githubButton).toBeVisible();
|
||||
}
|
||||
await expect(this.samlButton).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyNavigationLinks(): Promise<void> {
|
||||
await expect(this.page.getByText("Need to create an account?")).toBeVisible();
|
||||
await expect(this.signUpLink).toBeVisible();
|
||||
}
|
||||
|
||||
async verifySuccessfulLogin(): Promise<void> {
|
||||
await this.homePage.verifyPageLoaded();
|
||||
}
|
||||
|
||||
async verifyLoginError(errorMessage: string = "Invalid email or password"): Promise<void> {
|
||||
await expect(this.page.getByText(errorMessage).first()).toBeVisible();
|
||||
await expect(this.page).toHaveURL("/sign-in");
|
||||
}
|
||||
|
||||
async verifySamlModeActive(): Promise<void> {
|
||||
await expect(this.samlModeTitle).toBeVisible();
|
||||
await expect(this.passwordInput).not.toBeVisible();
|
||||
await expect(this.backButton).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyNormalModeActive(): Promise<void> {
|
||||
await expect(this.title).toBeVisible();
|
||||
await expect(this.passwordInput).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyLoadingState(): Promise<void> {
|
||||
await expect(this.loginButton).toHaveAttribute("aria-disabled", "true");
|
||||
await expect(this.loadingIndicator).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyFormValidation(): Promise<void> {
|
||||
// Check for common validation messages
|
||||
const emailError = this.page.getByText("Please enter a valid email address.");
|
||||
const passwordError = this.page.getByText("Password is required.");
|
||||
|
||||
// At least one validation error should be visible
|
||||
await expect(emailError.or(passwordError)).toBeVisible();
|
||||
}
|
||||
|
||||
// Accessibility methods
|
||||
async verifyKeyboardNavigation(): Promise<void> {
|
||||
// Test tab navigation through form elements
|
||||
await this.page.keyboard.press("Tab"); // Theme toggle
|
||||
await this.page.keyboard.press("Tab"); // Email field
|
||||
await expect(this.emailInput).toBeFocused();
|
||||
|
||||
await this.page.keyboard.press("Tab"); // Password field
|
||||
await expect(this.passwordInput).toBeFocused();
|
||||
|
||||
await this.page.keyboard.press("Tab"); // Show password button
|
||||
await this.page.keyboard.press("Tab"); // Login button
|
||||
await expect(this.loginButton).toBeFocused();
|
||||
}
|
||||
|
||||
async verifyAriaLabels(): Promise<void> {
|
||||
await expect(this.page.getByRole("textbox", { name: "Email" })).toBeVisible();
|
||||
await expect(this.page.getByRole("textbox", { name: "Password" })).toBeVisible();
|
||||
await expect(this.page.getByRole("button", { name: "Log in" })).toBeVisible();
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
async clearForm(): Promise<void> {
|
||||
await this.emailInput.clear();
|
||||
await this.passwordInput.clear();
|
||||
}
|
||||
|
||||
async isFormValid(): Promise<boolean> {
|
||||
const emailValue = await this.emailInput.inputValue();
|
||||
const passwordValue = await this.passwordInput.inputValue();
|
||||
return emailValue.length > 0 && passwordValue.length > 0;
|
||||
}
|
||||
|
||||
async getFormErrors(): Promise<string[]> {
|
||||
const errorElements = await this.errorMessages.all();
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const element of errorElements) {
|
||||
const text = await element.textContent();
|
||||
if (text) {
|
||||
errors.push(text.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Browser interaction methods
|
||||
async refresh(): Promise<void> {
|
||||
await this.page.reload();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async goBack(): Promise<void> {
|
||||
await this.page.goBack();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
// Session management methods
|
||||
async logout(): Promise<void> {
|
||||
await this.homePage.signOut();
|
||||
}
|
||||
|
||||
async verifyLogoutSuccess(): Promise<void> {
|
||||
await expect(this.page).toHaveURL("/sign-in");
|
||||
await expect(this.title).toBeVisible();
|
||||
}
|
||||
|
||||
// Advanced interaction methods
|
||||
async fillFormWithValidation(credentials: SignInCredentials): Promise<void> {
|
||||
// Fill email first and check for validation
|
||||
await this.fillEmail(credentials.email);
|
||||
await this.page.keyboard.press("Tab"); // Trigger validation
|
||||
|
||||
// Fill password
|
||||
await this.fillPassword(credentials.password);
|
||||
}
|
||||
|
||||
async submitFormWithEnterKey(): Promise<void> {
|
||||
await this.passwordInput.press("Enter");
|
||||
}
|
||||
|
||||
async submitFormWithButtonClick(): Promise<void> {
|
||||
await this.submitForm();
|
||||
}
|
||||
|
||||
// Error handling methods
|
||||
async handleSamlError(): Promise<void> {
|
||||
const samlError = this.page.getByText("SAML Authentication Error");
|
||||
if (await samlError.isVisible()) {
|
||||
// Handle SAML error if present
|
||||
console.log("SAML authentication error detected");
|
||||
}
|
||||
}
|
||||
|
||||
// Wait methods
|
||||
async waitForFormSubmission(): Promise<void> {
|
||||
await this.page.waitForFunction(() => {
|
||||
const button = document.querySelector('button[aria-disabled="true"]');
|
||||
return button === null;
|
||||
});
|
||||
}
|
||||
|
||||
async waitForRedirect(expectedUrl: string): Promise<void> {
|
||||
await this.page.waitForURL(expectedUrl);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ authManageAccountSetup('authenticate as manage account e2e user', async ({ page
|
||||
const accountEmail = process.env.E2E_MANAGE_ACCOUNT_USER;
|
||||
const accountPassword = process.env.E2E_MANAGE_ACCOUNT_PASSWORD;
|
||||
|
||||
|
||||
if (!accountEmail || !accountPassword) {
|
||||
throw new Error('E2E_MANAGE_ACCOUNT_USER and E2E_MANAGE_ACCOUNT_PASSWORD environment variables are required');
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ authManageCloudProvidersSetup('authenticate as manage cloud providers e2e user',
|
||||
const cloudProvidersEmail = process.env.E2E_MANAGE_CLOUD_PROVIDERS_USER;
|
||||
const cloudProvidersPassword = process.env.E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD;
|
||||
|
||||
|
||||
if (!cloudProvidersEmail || !cloudProvidersPassword) {
|
||||
throw new Error('E2E_MANAGE_CLOUD_PROVIDERS_USER and E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD environment variables are required');
|
||||
}
|
||||
|
||||
+37
-1
@@ -1,4 +1,12 @@
|
||||
export type RequirementStatus = "PASS" | "FAIL" | "MANUAL" | "No findings";
|
||||
export const REQUIREMENT_STATUS = {
|
||||
PASS: "PASS",
|
||||
FAIL: "FAIL",
|
||||
MANUAL: "MANUAL",
|
||||
NO_FINDINGS: "No findings",
|
||||
} as const;
|
||||
|
||||
export type RequirementStatus =
|
||||
(typeof REQUIREMENT_STATUS)[keyof typeof REQUIREMENT_STATUS];
|
||||
|
||||
export interface CompliancesOverview {
|
||||
data: ComplianceOverviewData[];
|
||||
@@ -133,6 +141,14 @@ export interface KISAAttributesMetadata {
|
||||
NonComplianceCases: string[];
|
||||
}
|
||||
|
||||
export interface C5AttributesMetadata {
|
||||
Section: string;
|
||||
SubSection: string;
|
||||
Type: string;
|
||||
AboutCriteria: string;
|
||||
ComplementaryCriteria: string;
|
||||
}
|
||||
|
||||
export interface MITREAttributesMetadata {
|
||||
// Dynamic cloud service field - could be AWSService, GCPService, AzureService, etc.
|
||||
[key: string]: string;
|
||||
@@ -150,6 +166,24 @@ export interface GenericAttributesMetadata {
|
||||
Type: string | null;
|
||||
}
|
||||
|
||||
export interface CCCAttributesMetadata {
|
||||
FamilyName: string;
|
||||
FamilyDescription: string;
|
||||
Section: string;
|
||||
SubSection: string;
|
||||
SubSectionObjective: string;
|
||||
Applicability: string[];
|
||||
Recommendation: string;
|
||||
SectionThreatMappings: Array<{
|
||||
ReferenceId: string;
|
||||
Identifiers: string[];
|
||||
}>;
|
||||
SectionGuidelineMappings: Array<{
|
||||
ReferenceId: string;
|
||||
Identifiers: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AttributesItemData {
|
||||
type: "compliance-requirements-attributes";
|
||||
id: string;
|
||||
@@ -167,7 +201,9 @@ export interface AttributesItemData {
|
||||
| AWSWellArchitectedAttributesMetadata[]
|
||||
| ThreatAttributesMetadata[]
|
||||
| KISAAttributesMetadata[]
|
||||
| C5AttributesMetadata[]
|
||||
| MITREAttributesMetadata[]
|
||||
| CCCAttributesMetadata[]
|
||||
| GenericAttributesMetadata[];
|
||||
check_ids: string[];
|
||||
// MITRE structure
|
||||
|
||||
+14
-5
@@ -1,5 +1,5 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { SVGProps } from "react";
|
||||
import { MouseEvent, SVGProps } from "react";
|
||||
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
|
||||
@@ -21,7 +21,7 @@ export type SubmenuProps = {
|
||||
active?: boolean;
|
||||
icon: IconComponent;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
||||
};
|
||||
|
||||
export type MenuProps = {
|
||||
@@ -213,15 +213,24 @@ export type AzureCredentials = {
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type M365Credentials = {
|
||||
export type M365ClientSecretCredentials = {
|
||||
[ProviderCredentialFields.CLIENT_ID]: string;
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: string;
|
||||
[ProviderCredentialFields.TENANT_ID]: string;
|
||||
[ProviderCredentialFields.USER]?: string;
|
||||
[ProviderCredentialFields.PASSWORD]?: string;
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type M365CertificateCredentials = {
|
||||
[ProviderCredentialFields.CLIENT_ID]: string;
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: string;
|
||||
[ProviderCredentialFields.TENANT_ID]: string;
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type M365Credentials =
|
||||
| M365ClientSecretCredentials
|
||||
| M365CertificateCredentials;
|
||||
|
||||
export type GCPDefaultCredentials = {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
|
||||
+30
-26
@@ -168,12 +168,13 @@ export const addCredentialsFormSchema = (
|
||||
.min(1, "Client ID is required"),
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: z
|
||||
.string()
|
||||
.min(1, "Client Secret is required"),
|
||||
.optional(),
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: z
|
||||
.string()
|
||||
.optional(),
|
||||
[ProviderCredentialFields.TENANT_ID]: z
|
||||
.string()
|
||||
.min(1, "Tenant ID is required"),
|
||||
[ProviderCredentialFields.USER]: z.string().optional(),
|
||||
[ProviderCredentialFields.PASSWORD]: z.string().optional(),
|
||||
}
|
||||
: providerType === "github"
|
||||
? {
|
||||
@@ -194,23 +195,26 @@ export const addCredentialsFormSchema = (
|
||||
})
|
||||
.superRefine((data: Record<string, any>, ctx) => {
|
||||
if (providerType === "m365") {
|
||||
const hasUser = !!data[ProviderCredentialFields.USER];
|
||||
const hasPassword = !!data[ProviderCredentialFields.PASSWORD];
|
||||
|
||||
if (hasUser && !hasPassword) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "If you provide a user, you must also provide a password",
|
||||
path: [ProviderCredentialFields.PASSWORD],
|
||||
});
|
||||
}
|
||||
|
||||
if (hasPassword && !hasUser) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "If you provide a password, you must also provide a user",
|
||||
path: [ProviderCredentialFields.USER],
|
||||
});
|
||||
// Validate based on the via parameter
|
||||
if (via === "app_client_secret") {
|
||||
const clientSecret = data[ProviderCredentialFields.CLIENT_SECRET];
|
||||
if (!clientSecret || clientSecret.trim() === "") {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Client Secret is required",
|
||||
path: [ProviderCredentialFields.CLIENT_SECRET],
|
||||
});
|
||||
}
|
||||
} else if (via === "app_certificate") {
|
||||
const certificateContent =
|
||||
data[ProviderCredentialFields.CERTIFICATE_CONTENT];
|
||||
if (!certificateContent || certificateContent.trim() === "") {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Certificate Content is required",
|
||||
path: [ProviderCredentialFields.CERTIFICATE_CONTENT],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +223,7 @@ export const addCredentialsFormSchema = (
|
||||
if (via === "personal_access_token") {
|
||||
if (!data[ProviderCredentialFields.PERSONAL_ACCESS_TOKEN]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "Personal Access Token is required",
|
||||
path: [ProviderCredentialFields.PERSONAL_ACCESS_TOKEN],
|
||||
});
|
||||
@@ -227,7 +231,7 @@ export const addCredentialsFormSchema = (
|
||||
} else if (via === "oauth_app") {
|
||||
if (!data[ProviderCredentialFields.OAUTH_APP_TOKEN]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "OAuth App Token is required",
|
||||
path: [ProviderCredentialFields.OAUTH_APP_TOKEN],
|
||||
});
|
||||
@@ -235,14 +239,14 @@ export const addCredentialsFormSchema = (
|
||||
} else if (via === "github_app") {
|
||||
if (!data[ProviderCredentialFields.GITHUB_APP_ID]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "GitHub App ID is required",
|
||||
path: [ProviderCredentialFields.GITHUB_APP_ID],
|
||||
});
|
||||
}
|
||||
if (!data[ProviderCredentialFields.GITHUB_APP_KEY]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "GitHub App Private Key is required",
|
||||
path: [ProviderCredentialFields.GITHUB_APP_KEY],
|
||||
});
|
||||
@@ -390,7 +394,7 @@ export const mutedFindingsConfigFormSchema = z.object({
|
||||
const yamlValidation = validateYaml(val);
|
||||
if (!yamlValidation.isValid) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: `Invalid YAML format: ${yamlValidation.error}`,
|
||||
});
|
||||
return;
|
||||
@@ -399,7 +403,7 @@ export const mutedFindingsConfigFormSchema = z.object({
|
||||
const mutelistValidation = validateMutelistYaml(val);
|
||||
if (!mutelistValidation.isValid) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: `Invalid mutelist structure: ${mutelistValidation.error}`,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user