-
+
{item.name}
-
+
{item.value}%
@@ -67,8 +65,8 @@ const CustomTooltip = ({ active, payload }: any) => {
export function RadialChart({
percentage,
- color = "var(--chart-success-color)",
- backgroundColor = CHART_COLORS.tooltipBackground,
+ color = "var(--bg-pass-primary)",
+ backgroundColor = "var(--bg-neutral-tertiary)",
height = 250,
innerRadius = 60,
outerRadius = 100,
@@ -154,24 +152,18 @@ export function RadialChart({
const y = centerY - middleRadius * Math.sin(currentAngleRad);
return (
-
+
);
})}
{percentage}%
diff --git a/ui/components/graphs/shared/constants.ts b/ui/components/graphs/shared/constants.ts
index 4aadd4aac0..22fc946ff3 100644
--- a/ui/components/graphs/shared/constants.ts
+++ b/ui/components/graphs/shared/constants.ts
@@ -1,10 +1,9 @@
export const SEVERITY_COLORS = {
- Informational: "var(--chart-info)",
- Info: "var(--chart-info)",
- Low: "var(--chart-warning)",
- Medium: "var(--chart-warning-emphasis)",
- High: "var(--chart-danger)",
- Critical: "var(--chart-danger-emphasis)",
+ Informational: "var(--bg-data-info)",
+ Low: "var(--bg-data-low)",
+ Medium: "var(--bg-data-medium)",
+ High: "var(--bg-data-high)",
+ Critical: "var(--bg-data-critical)",
} as const;
export const PROVIDER_COLORS = {
diff --git a/ui/components/shadcn/card/base-card/base-card.tsx b/ui/components/shadcn/card/base-card/base-card.tsx
deleted file mode 100644
index 946e0cc184..0000000000
--- a/ui/components/shadcn/card/base-card/base-card.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "@/lib/utils";
-
-import { Card } from "../card";
-
-const baseCardVariants = cva("", {
- variants: {
- variant: {
- default:
- "border-slate-200 bg-white dark:border-zinc-900 dark:bg-stone-950",
- },
- },
- defaultVariants: {
- variant: "default",
- },
-});
-
-interface BaseCardProps
- extends React.ComponentProps,
- VariantProps {}
-
-const BaseCard = ({ className, variant, ...props }: BaseCardProps) => {
- return (
-
- );
-};
-
-export { BaseCard };
diff --git a/ui/components/shadcn/card/card.tsx b/ui/components/shadcn/card/card.tsx
index 62f4c66865..d57ddf0eb7 100644
--- a/ui/components/shadcn/card/card.tsx
+++ b/ui/components/shadcn/card/card.tsx
@@ -1,3 +1,5 @@
+import { cva, type VariantProps } from "class-variance-authority";
+
import { cn } from "@/lib/utils";
export const CardVariant = {
@@ -10,14 +12,44 @@ export const CardVariant = {
export type CardVariant = (typeof CardVariant)[keyof typeof CardVariant];
-function Card({ className, ...props }: React.ComponentProps<"div">) {
+const cardVariants = cva("flex flex-col gap-6 rounded-xl border", {
+ variants: {
+ variant: {
+ default: "",
+ base: "border-border-neutral-secondary bg-bg-neutral-secondary px-[18px] pt-3 pb-4",
+ inner:
+ "rounded-[12px] backdrop-blur-[46px] border-border-neutral-tertiary bg-bg-neutral-tertiary",
+ },
+ padding: {
+ default: "",
+ sm: "px-3 py-2",
+ md: "px-4 py-3",
+ lg: "px-5 py-4",
+ none: "p-0",
+ },
+ },
+ compoundVariants: [
+ {
+ variant: "inner",
+ padding: "default",
+ className: "px-4 py-3", // md padding by default for inner
+ },
+ ],
+ defaultVariants: {
+ variant: "default",
+ padding: "default",
+ },
+});
+
+interface CardProps
+ extends React.ComponentProps<"div">,
+ VariantProps {}
+
+function Card({ className, variant, padding, ...props }: CardProps) {
return (
);
@@ -28,7 +60,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
) {
return (
);
@@ -96,4 +125,6 @@ export {
CardFooter,
CardHeader,
CardTitle,
+ cardVariants,
};
+export type { CardProps };
diff --git a/ui/components/shadcn/card/resource-stats-card/resource-stats-card-container.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-container.tsx
deleted file mode 100644
index de27d052ed..0000000000
--- a/ui/components/shadcn/card/resource-stats-card/resource-stats-card-container.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "@/lib/utils";
-
-const containerVariants = cva(
- [
- "flex",
- "rounded-[12px]",
- "border",
- "backdrop-blur-[46px]",
- "border-slate-300",
- "bg-[#F8FAFC80]",
- "dark:border-[rgba(38,38,38,0.70)]",
- "dark:bg-[rgba(23,23,23,0.50)]",
- ],
- {
- variants: {
- padding: {
- sm: "px-3 py-2",
- md: "px-[19px] py-[9px]",
- lg: "px-6 py-3",
- none: "p-0",
- },
- },
- defaultVariants: {
- padding: "md",
- },
- },
-);
-
-export interface ResourceStatsCardContainerProps
- extends React.HTMLAttributes
,
- VariantProps {
- ref?: React.Ref;
-}
-
-export const ResourceStatsCardContainer = ({
- className,
- children,
- padding,
- ref,
- ...props
-}: ResourceStatsCardContainerProps) => {
- return (
-
- {children}
-
- );
-};
-
-ResourceStatsCardContainer.displayName = "ResourceStatsCardContainer";
diff --git a/ui/components/shadcn/card/resource-stats-card/resource-stats-card-content.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-content.tsx
index e338c5e7b1..be3080143d 100644
--- a/ui/components/shadcn/card/resource-stats-card/resource-stats-card-content.tsx
+++ b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-content.tsx
@@ -11,11 +11,11 @@ export interface StatItem {
}
const variantColors = {
- default: "#868994",
- fail: "#f54280",
- pass: "#4ade80",
- warning: "#fbbf24",
- info: "#60a5fa",
+ default: "var(--bg-neutral-tertiary)",
+ fail: "var(--bg-fail-primary)",
+ pass: "var(--bg-pass-primary)",
+ warning: "var(--bg-warning-primary)",
+ info: "var(--bg-data-info)",
} as const;
type BadgeVariant = keyof typeof variantColors;
@@ -26,8 +26,8 @@ const badgeVariants = cva(
variants: {
variant: {
[CardVariant.default]: "bg-slate-100 dark:bg-[#535359]",
- [CardVariant.fail]: "bg-red-100 dark:bg-[#432232]",
- [CardVariant.pass]: "bg-green-100 dark:bg-[#204237]",
+ [CardVariant.fail]: "bg-bg-fail-secondary",
+ [CardVariant.pass]: "bg-bg-pass-secondary",
[CardVariant.warning]: "bg-amber-100 dark:bg-[#3d3520]",
[CardVariant.info]: "bg-blue-100 dark:bg-[#1e3a5f]",
},
@@ -58,7 +58,7 @@ const badgeIconVariants = cva("", {
});
const labelTextVariants = cva(
- "leading-6 font-semibold text-slate-900 dark:text-zinc-300 whitespace-nowrap",
+ "leading-6 font-semibold text-text-neutral-secondary whitespace-nowrap",
{
variants: {
size: {
@@ -73,7 +73,7 @@ const labelTextVariants = cva(
},
);
-const statIconVariants = cva("text-slate-600 dark:text-zinc-300", {
+const statIconVariants = cva("text-text-neutral-secondary", {
variants: {
size: {
sm: "h-2.5 w-2.5",
@@ -87,7 +87,7 @@ const statIconVariants = cva("text-slate-600 dark:text-zinc-300", {
});
const statLabelVariants = cva(
- "leading-5 font-medium text-slate-700 dark:text-zinc-300",
+ "leading-5 font-medium text-text-neutral-secondary",
{
variants: {
size: {
diff --git a/ui/components/shadcn/card/resource-stats-card/resource-stats-card-divider.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-divider.tsx
deleted file mode 100644
index 3ec8d57ee4..0000000000
--- a/ui/components/shadcn/card/resource-stats-card/resource-stats-card-divider.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "@/lib/utils";
-
-const dividerVariants = cva("flex items-center justify-center", {
- variants: {
- spacing: {
- sm: "px-2",
- md: "px-[23px]",
- lg: "px-8",
- },
- orientation: {
- vertical: "h-full",
- horizontal: "w-full",
- },
- },
- defaultVariants: {
- spacing: "md",
- orientation: "vertical",
- },
-});
-
-const lineVariants = cva("bg-[rgba(39,39,42,1)]", {
- variants: {
- orientation: {
- vertical: "h-full w-px",
- horizontal: "w-full h-px",
- },
- },
- defaultVariants: {
- orientation: "vertical",
- },
-});
-
-export interface ResourceStatsCardDividerProps
- extends React.HTMLAttributes,
- VariantProps {
- ref?: React.Ref;
-}
-
-export const ResourceStatsCardDivider = ({
- className,
- spacing,
- orientation,
- ref,
- ...props
-}: ResourceStatsCardDividerProps) => {
- return (
-
- );
-};
-
-ResourceStatsCardDivider.displayName = "ResourceStatsCardDivider";
diff --git a/ui/components/shadcn/card/resource-stats-card/resource-stats-card-header.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-header.tsx
index 2a4068e998..c324376a04 100644
--- a/ui/components/shadcn/card/resource-stats-card/resource-stats-card-header.tsx
+++ b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-header.tsx
@@ -16,7 +16,7 @@ const headerVariants = cva("flex w-full items-center gap-1", {
},
});
-const iconVariants = cva("text-zinc-300 dark:text-zinc-300", {
+const iconVariants = cva("text-text-neutral-secondary", {
variants: {
size: {
sm: "h-3.5 w-3.5",
@@ -30,7 +30,7 @@ const iconVariants = cva("text-zinc-300 dark:text-zinc-300", {
});
const titleVariants = cva(
- "leading-7 font-semibold text-zinc-300 dark:text-zinc-300",
+ "leading-7 font-semibold text-text-neutral-secondary",
{
variants: {
size: {
@@ -45,21 +45,18 @@ const titleVariants = cva(
},
);
-const countVariants = cva(
- "leading-4 font-normal text-zinc-300 dark:text-zinc-300",
- {
- variants: {
- size: {
- sm: "text-[9px]",
- md: "text-[10px]",
- lg: "text-xs",
- },
- },
- defaultVariants: {
- size: "md",
+const countVariants = cva("leading-4 font-normal text-text-neutral-secondary", {
+ variants: {
+ size: {
+ sm: "text-[9px]",
+ md: "text-[10px]",
+ lg: "text-xs",
},
},
-);
+ defaultVariants: {
+ size: "md",
+ },
+});
export interface ResourceStatsCardHeaderProps
extends React.HTMLAttributes,
diff --git a/ui/components/shadcn/card/resource-stats-card/resource-stats-card.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card.tsx
index 99d3e7867d..bb42f4bfff 100644
--- a/ui/components/shadcn/card/resource-stats-card/resource-stats-card.tsx
+++ b/ui/components/shadcn/card/resource-stats-card/resource-stats-card.tsx
@@ -3,17 +3,12 @@ import { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
-import { CardVariant } from "../card";
-import { ResourceStatsCardContainer } from "./resource-stats-card-container";
+import { Card, CardVariant } from "../card";
import type { StatItem } from "./resource-stats-card-content";
import { ResourceStatsCardContent } from "./resource-stats-card-content";
import { ResourceStatsCardHeader } from "./resource-stats-card-header";
export type { StatItem };
-
-// Todo: when the design system is ready, we must use the colors from the design system (semantic colors)
-// Variant styles using CVA for type safety and consistency
-// Colors are exact HEX values from Figma design system
const cardVariants = cva("", {
variants: {
variant: {
@@ -109,7 +104,7 @@ export const ResourceStatsCard = ({
{header && }
{emptyState ? (
@@ -131,15 +126,16 @@ export const ResourceStatsCard = ({
// Otherwise, render with container
return (
-
{header && }
{emptyState ? (
@@ -155,7 +151,7 @@ export const ResourceStatsCard = ({
/>
)
)}
-
+
);
};
diff --git a/ui/components/shadcn/index.ts b/ui/components/shadcn/index.ts
index 4bb244fb95..8991f1e34d 100644
--- a/ui/components/shadcn/index.ts
+++ b/ui/components/shadcn/index.ts
@@ -1,14 +1,12 @@
export * from "./badge/badge";
export * from "./button/button";
-export * from "./card/base-card/base-card";
export * from "./card/card";
export * from "./card/resource-stats-card/resource-stats-card";
-export * from "./card/resource-stats-card/resource-stats-card-container";
export * from "./card/resource-stats-card/resource-stats-card-content";
-export * from "./card/resource-stats-card/resource-stats-card-divider";
export * from "./card/resource-stats-card/resource-stats-card-header";
export * from "./dropdown/dropdown";
export * from "./select/select";
export * from "./separator/separator";
+export * from "./skeleton/skeleton";
export * from "./tabs/generic-tabs";
export * from "./tabs/tabs";
diff --git a/ui/components/shadcn/skeleton/skeleton.tsx b/ui/components/shadcn/skeleton/skeleton.tsx
new file mode 100644
index 0000000000..34971c0554
--- /dev/null
+++ b/ui/components/shadcn/skeleton/skeleton.tsx
@@ -0,0 +1,16 @@
+import { cn } from "@/lib/utils";
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export { Skeleton };
diff --git a/ui/styles/globals.css b/ui/styles/globals.css
index 50272c81a4..d6dfe3ee05 100644
--- a/ui/styles/globals.css
+++ b/ui/styles/globals.css
@@ -6,11 +6,11 @@
/* ===== LIGHT THEME (ROOT) ===== */
:root {
/* ===== LEGACY VARIABLES (CHART COLORS) ===== */
- --chart-info: #3C8DFF;
- --chart-warning: #FDFBD4;
- --chart-warning-emphasis: #FEC94D;
- --chart-danger: #F77852;
- --chart-danger-emphasis: #FF006A;
+ --chart-info: #3c8dff;
+ --chart-warning: #fdfbd4;
+ --chart-warning-emphasis: #fec94d;
+ --chart-danger: #f77852;
+ --chart-danger-emphasis: #ff006a;
--chart-success-color: #16a34a;
--chart-fail: #dc2626;
--chart-radar-primary: #9d174d;
@@ -69,36 +69,40 @@
/* Text Colors */
--text-neutral-primary: var(--color-slate-950);
- --text-neutral-secondary: var(--color-zinc-700);
+ --text-neutral-secondary: var(--color-zinc-800);
--text-neutral-tertiary: var(--color-zinc-500);
--text-error-primary: var(--color-red-600);
/* Background Colors */
- --bg-neutral-primary: #FDFDFD;
+ --bg-neutral-primary: #fdfdfd;
--bg-neutral-secondary: var(--color-white);
- --bg-neutral-tertiary: #FBFDFD;
+ --bg-neutral-tertiary: #fbfdfd;
--bg-tag-primary: var(--color-slate-50);
--bg-pass-primary: var(--color-emerald-400);
--bg-pass-secondary: var(--color-emerald-50);
+ --bg-warning-primary: var(--color-orange-500);
--bg-fail-primary: var(--color-rose-500);
--bg-fail-secondary: var(--color-rose-50);
/* Severity Colors */
- --bg-data-critical: #FF006A;
- --bg-data-high: #F77852;
- --bg-data-medium: #FDD34F;
- --bg-data-low: #F5F3CE;
- --bg-data-info: #3C8DFF;
+ --bg-data-critical: #ff006a;
+ --bg-data-high: #f77852;
+ --bg-data-medium: #fdd34f;
+ --bg-data-low: #f5f3ce;
+ --bg-data-info: #3c8dff;
+
+ /* Chart Dots */
+ --chart-dots: var(--color-neutral-200);
}
/* ===== DARK THEME ===== */
.dark {
/* ===== LEGACY VARIABLES (CHART COLORS) ===== */
- --chart-info: #3C8DFF;
- --chart-warning: #FDFBD4;
- --chart-warning-emphasis: #FEC94D;
- --chart-danger: #F77852;
- --chart-danger-emphasis: #FF006A;
+ --chart-info: #3c8dff;
+ --chart-warning: #fdfbd4;
+ --chart-warning-emphasis: #fec94d;
+ --chart-danger: #f77852;
+ --chart-danger-emphasis: #ff006a;
--chart-success-color: #86da26;
--chart-fail: #db2b49;
--chart-radar-primary: #b51c80;
@@ -157,26 +161,30 @@
/* Text Colors */
--text-neutral-primary: var(--color-zinc-100);
- --text-neutral-secondary: var(--color-zinc-400);
+ --text-neutral-secondary: var(--color-zinc-300);
--text-neutral-tertiary: var(--color-zinc-500);
--text-error-primary: var(--color-rose-300);
/* Background Colors */
--bg-neutral-primary: var(--color-zinc-950);
- --bg-neutral-secondary: var(--color-slate-950);
+ --bg-neutral-secondary: var(--color-stone-950);
--bg-neutral-tertiary: #121110;
--bg-tag-primary: var(--color-slate-950);
+ --bg-warning-primary: var(--color-orange-400);
--bg-pass-primary: var(--color-green-400);
--bg-pass-secondary: var(--color-emerald-900);
--bg-fail-primary: var(--color-rose-500);
--bg-fail-secondary: #432232;
/* Severity Colors */
- --bg-data-critical: #FF006A;
- --bg-data-high: #F77852;
- --bg-data-medium: #FEC94D;
- --bg-data-low: #FDFBD4;
- --bg-data-info: #3C8DFF;
+ --bg-data-critical: #ff006a;
+ --bg-data-high: #f77852;
+ --bg-data-medium: #fec94d;
+ --bg-data-low: #fdfbd4;
+ --bg-data-info: #3c8dff;
+
+ /* Chart Dots */
+ --chart-dots: var(--text-neutral-primary);
}
/* ===== TAILWIND THEME MAPPINGS ===== */
@@ -248,6 +256,7 @@
--color-bg-tag: var(--bg-tag-primary);
--color-bg-pass: var(--bg-pass-primary);
--color-bg-pass-secondary: var(--bg-pass-secondary);
+ --color-bg-warning: var(--bg-warning-primary);
--color-bg-fail: var(--bg-fail-primary);
--color-bg-fail-secondary: var(--bg-fail-secondary);
}
@@ -357,4 +366,4 @@
body {
@apply bg-background text-foreground;
}
-}
\ No newline at end of file
+}
From ef4e28da03ebae1ba06cda9123aa3e5216bd817c Mon Sep 17 00:00:00 2001
From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
Date: Mon, 10 Nov 2025 11:23:56 +0100
Subject: [PATCH 02/23] fix(m365_powershell): teams connection with
`--sp-env-auth` and enhanced timeouts error logging (#9191)
---
prowler/CHANGELOG.md | 1 +
.../m365/lib/powershell/m365_powershell.py | 41 +++---
.../lib/powershell/m365_powershell_test.py | 118 ++++++++----------
3 files changed, 66 insertions(+), 94 deletions(-)
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index 7ee85b6607..286322a155 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -49,6 +49,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Fixed
- Check `check_name` has no `resource_name` error for GCP provider [(#9169)](https://github.com/prowler-cloud/prowler/pull/9169)
- Depth Truncation and parsing error in PowerShell queries [(#9181)](https://github.com/prowler-cloud/prowler/pull/9181)
+- Fix M365 Teams `--sp-env-auth` connection error and enhanced timeout logging [(#9191)](https://github.com/prowler-cloud/prowler/pull/9191)
---
diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py
index e3d729275f..797fd667ce 100644
--- a/prowler/providers/m365/lib/powershell/m365_powershell.py
+++ b/prowler/providers/m365/lib/powershell/m365_powershell.py
@@ -1,5 +1,4 @@
import os
-from typing import Optional
from prowler.lib.logger import logger
from prowler.lib.powershell.powershell import PowerShellSession
@@ -7,12 +6,11 @@ from prowler.providers.m365.exceptions.exceptions import (
M365CertificateCreationError,
M365GraphConnectionError,
)
-from prowler.providers.m365.lib.jwt.jwt_decoder import decode_jwt, decode_msal_token
+from prowler.providers.m365.lib.jwt.jwt_decoder import decode_msal_token
from prowler.providers.m365.models import M365Credentials, M365IdentityInfo
class M365PowerShell(PowerShellSession):
- CONNECT_TIMEOUT = 15
"""
Microsoft 365 specific PowerShell session management implementation.
@@ -125,9 +123,7 @@ class M365PowerShell(PowerShellSession):
'$graphToken = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $graphtokenBody | Select-Object -ExpandProperty Access_Token'
)
- def _execute_connect_command(
- self, command: str, timeout: Optional[int] = None
- ) -> str:
+ def execute_connect(self, command: str) -> str:
"""
Execute a PowerShell connect command ensuring empty responses surface as timeouts.
@@ -138,9 +134,9 @@ class M365PowerShell(PowerShellSession):
Returns:
str: Command output or 'Timeout' if the command produced no output.
"""
- effective_timeout = timeout or self.CONNECT_TIMEOUT
- result = self.execute(command, timeout=effective_timeout)
- return result or "Timeout"
+ connect_timeout = 15
+ result = self.execute(command, timeout=connect_timeout)
+ return result or "'execute_connect' command timeout reached"
def test_credentials(self, credentials: M365Credentials) -> bool:
"""
@@ -207,7 +203,7 @@ class M365PowerShell(PowerShellSession):
def test_graph_certificate_connection(self) -> bool:
"""Test Microsoft Graph API connection using certificate and raise exception if it fails."""
- result = self._execute_connect_command(
+ result = self.execute_connect(
"Connect-Graph -Certificate $certificate -AppId $clientID -TenantId $tenantID"
)
if "Welcome to Microsoft Graph!" not in result:
@@ -221,18 +217,13 @@ class M365PowerShell(PowerShellSession):
self.execute(
'$teamstokenBody = @{ Grant_Type = "client_credentials"; Scope = "48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default"; Client_Id = $clientID; Client_Secret = $clientSecret }'
)
- self.execute(
+ result = self.execute(
'$teamsToken = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token" -Method POST -Body $teamstokenBody | Select-Object -ExpandProperty Access_Token'
)
- permissions = decode_jwt(self.execute("Write-Output $teamsToken")).get(
- "roles", []
- )
- if "application_access" not in permissions:
- logger.error(
- "Microsoft Teams connection failed: Please check your permissions and try again."
- )
+ if result != "":
+ logger.error(f"Microsoft Teams connection failed: {result}")
return False
- self._execute_connect_command(
+ self.execute_connect(
'Connect-MicrosoftTeams -AccessTokens @("$graphToken","$teamsToken")'
)
return True
@@ -244,7 +235,7 @@ class M365PowerShell(PowerShellSession):
def test_teams_certificate_connection(self) -> bool:
"""Test Microsoft Teams API connection using certificate and raise exception if it fails."""
- result = self._execute_connect_command(
+ result = self.execute_connect(
"Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID"
)
if self.tenant_identity.identity_id not in result:
@@ -268,9 +259,8 @@ class M365PowerShell(PowerShellSession):
"Exchange Online connection failed: Please check your permissions and try again."
)
return False
- self._execute_connect_command(
- 'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"',
- timeout=self.CONNECT_TIMEOUT,
+ self.execute_connect(
+ 'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"'
)
return True
except Exception as e:
@@ -281,9 +271,8 @@ class M365PowerShell(PowerShellSession):
def test_exchange_certificate_connection(self) -> bool:
"""Test Exchange Online API connection using certificate and raise exception if it fails."""
- result = self._execute_connect_command(
- "Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
- timeout=self.CONNECT_TIMEOUT,
+ result = self.execute_connect(
+ "Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
)
if "https://aka.ms/exov3-module" not in result:
logger.error(f"Exchange Online Certificate connection failed: {result}")
diff --git a/tests/providers/m365/lib/powershell/m365_powershell_test.py b/tests/providers/m365/lib/powershell/m365_powershell_test.py
index 8d37b6c095..28b09768c3 100644
--- a/tests/providers/m365/lib/powershell/m365_powershell_test.py
+++ b/tests/providers/m365/lib/powershell/m365_powershell_test.py
@@ -547,8 +547,7 @@ class Testm365PowerShell:
session.close()
@patch("subprocess.Popen")
- @patch("prowler.providers.m365.lib.powershell.m365_powershell.decode_jwt")
- def test_test_teams_connection_success(self, mock_decode_jwt, mock_popen):
+ def test_test_teams_connection_success(self, mock_popen):
"""Test test_teams_connection when token is valid"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
@@ -567,30 +566,20 @@ class Testm365PowerShell:
)
session = M365PowerShell(credentials, identity)
- # Mock execute to return valid responses
- def mock_execute(command, *args, **kwargs):
- if "Write-Output $teamsToken" in command:
- return "valid_teams_token"
- return None
-
- session.execute = MagicMock(side_effect=mock_execute)
- # Mock JWT decode to return proper permissions
- mock_decode_jwt.return_value = {"roles": ["application_access"]}
+ session.execute = MagicMock(side_effect=[None, ""])
+ session.execute_connect = MagicMock(return_value="")
result = session.test_teams_connection()
assert result is True
- # Verify all expected PowerShell commands were called
- # 4 calls: teamstokenBody, teamsToken, Write-Output $teamsToken, Connect-MicrosoftTeams
- assert session.execute.call_count == 4
- mock_decode_jwt.assert_called_once_with("valid_teams_token")
+ assert session.execute.call_count == 2
+ session.execute_connect.assert_called_once_with(
+ 'Connect-MicrosoftTeams -AccessTokens @("$graphToken","$teamsToken")'
+ )
session.close()
@patch("subprocess.Popen")
- @patch("prowler.providers.m365.lib.powershell.m365_powershell.decode_jwt")
- def test_test_teams_connection_missing_permissions(
- self, mock_decode_jwt, mock_popen
- ):
+ def test_test_teams_connection_missing_permissions(self, mock_popen):
"""Test test_teams_connection when token lacks required permissions"""
mock_process = MagicMock()
mock_popen.return_value = mock_process
@@ -609,23 +598,17 @@ class Testm365PowerShell:
)
session = M365PowerShell(credentials, identity)
- # Mock execute to return valid token but decode returns no permissions
- def mock_execute(command, *args, **kwargs):
- if "Write-Output $teamsToken" in command:
- return "valid_teams_token"
- return None
-
- session.execute = MagicMock(side_effect=mock_execute)
- # Mock JWT decode to return missing required permission
- mock_decode_jwt.return_value = {"roles": ["other_permission"]}
+ session.execute = MagicMock(side_effect=[None, "Permission denied"])
+ session.execute_connect = MagicMock()
with patch("prowler.lib.logger.logger.error") as mock_error:
result = session.test_teams_connection()
assert result is False
mock_error.assert_called_once_with(
- "Microsoft Teams connection failed: Please check your permissions and try again."
+ "Microsoft Teams connection failed: Permission denied"
)
+ session.execute_connect.assert_not_called()
session.close()
@patch("subprocess.Popen")
@@ -688,15 +671,17 @@ class Testm365PowerShell:
return None
session.execute = MagicMock(side_effect=mock_execute)
+ session.execute_connect = MagicMock(return_value=None)
# Mock MSAL token decode to return proper permissions
mock_decode_msal_token.return_value = {"roles": ["Exchange.ManageAsApp"]}
result = session.test_exchange_connection()
assert result is True
- # Verify all expected PowerShell commands were called
- # 4 calls: SecureSecret, exchangeToken, Write-Output $exchangeToken, Connect-ExchangeOnline
- assert session.execute.call_count == 4
+ assert session.execute.call_count == 3
+ session.execute_connect.assert_called_once_with(
+ 'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"'
+ )
mock_decode_msal_token.assert_called_once_with("valid_exchange_token")
session.close()
@@ -730,6 +715,7 @@ class Testm365PowerShell:
return None
session.execute = MagicMock(side_effect=mock_execute)
+ session.execute_connect = MagicMock(return_value=None)
# Mock MSAL token decode to return missing required permission
mock_decode_msal_token.return_value = {"roles": ["other_permission"]}
@@ -737,6 +723,7 @@ class Testm365PowerShell:
result = session.test_exchange_connection()
assert result is False
+ session.execute_connect.assert_not_called()
mock_error.assert_called_once_with(
"Exchange Online connection failed: Please check your permissions and try again."
)
@@ -781,7 +768,7 @@ class Testm365PowerShell:
mock_popen.return_value = mock_process
credentials = M365Credentials()
- identity = M365IdentityInfo()
+ identity = M365IdentityInfo(identity_id="expected-id")
session = M365PowerShell(credentials, identity)
# Test with clean base64 content
@@ -924,20 +911,18 @@ class Testm365PowerShell:
mock_popen.return_value = mock_process
credentials = M365Credentials()
- identity = M365IdentityInfo()
+ identity = M365IdentityInfo(identity_id="expected-id")
session = M365PowerShell(credentials, identity)
- # Mock successful Exchange connection
- session.execute = MagicMock(
+ session.execute_connect = MagicMock(
return_value="Connected successfully https://aka.ms/exov3-module"
)
result = session.test_exchange_certificate_connection()
assert result is True
- session.execute.assert_called_once_with(
- "Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
- timeout=M365PowerShell.CONNECT_TIMEOUT,
+ session.execute_connect.assert_called_once_with(
+ "Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
)
session.close()
@@ -949,20 +934,23 @@ class Testm365PowerShell:
mock_popen.return_value = mock_process
credentials = M365Credentials()
- identity = M365IdentityInfo()
+ identity = M365IdentityInfo(identity_id="expected-id")
session = M365PowerShell(credentials, identity)
- # Mock failed Exchange connection
- session.execute = MagicMock(
+ session.execute_connect = MagicMock(
return_value="Connection failed: Authentication error"
)
- result = session.test_exchange_certificate_connection()
+ with patch("prowler.lib.logger.logger.error") as mock_error:
+ result = session.test_exchange_certificate_connection()
+
assert result is False
- session.execute.assert_called_once_with(
- "Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain",
- timeout=M365PowerShell.CONNECT_TIMEOUT,
+ session.execute_connect.assert_called_once_with(
+ "Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain"
+ )
+ mock_error.assert_called_once_with(
+ "Exchange Online Certificate connection failed: Connection failed: Authentication error"
)
session.close()
@@ -981,20 +969,15 @@ class Testm365PowerShell:
session = M365PowerShell(credentials, identity)
# Mock successful Teams connection - the method returns bool
- def mock_execute_side_effect(command, *_, **__):
- if "Connect-MicrosoftTeams" in command:
- # Return result that contains the identity_id for success
- return "Connected successfully test_identity_id"
- return ""
-
- session.execute = MagicMock(side_effect=mock_execute_side_effect)
+ session.execute_connect = MagicMock(
+ return_value="Connected successfully test_identity_id"
+ )
result = session.test_teams_certificate_connection()
assert result is True
- session.execute.assert_called_once_with(
- "Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID",
- timeout=M365PowerShell.CONNECT_TIMEOUT,
+ session.execute_connect.assert_called_once_with(
+ "Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID"
)
session.close()
@@ -1006,22 +989,21 @@ class Testm365PowerShell:
mock_popen.return_value = mock_process
credentials = M365Credentials()
- identity = M365IdentityInfo()
+ identity = M365IdentityInfo(identity_id="expected-id")
session = M365PowerShell(credentials, identity)
- # Mock failed Teams connection
- def mock_execute_side_effect(command, **kwargs):
- if "Connect-MicrosoftTeams" in command:
- raise Exception("Connection failed: Authentication error")
- return ""
+ session.execute_connect = MagicMock(return_value="Connection failed")
- session.execute = MagicMock(side_effect=mock_execute_side_effect)
+ with patch("prowler.lib.logger.logger.error") as mock_error:
+ result = session.test_teams_certificate_connection()
- # Should raise exception on connection failure
- with pytest.raises(Exception) as exc_info:
- session.test_teams_certificate_connection()
-
- assert "Connection failed: Authentication error" in str(exc_info.value)
+ assert result is False
+ session.execute_connect.assert_called_once_with(
+ "Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID"
+ )
+ mock_error.assert_called_once_with(
+ "Microsoft Teams Certificate connection failed: Connection failed"
+ )
session.close()
From 789221d901fb705bfb3f60a00f65bf9e73098aa5 Mon Sep 17 00:00:00 2001
From: Ethan Troy <63926014+ethanolivertroy@users.noreply.github.com>
Date: Mon, 10 Nov 2025 08:41:18 -0500
Subject: [PATCH 03/23] feat(compliance): add FedRAMP 20x KSI Low compliance
frameworks (#9198)
Co-authored-by: pedrooot
---
README.md | 6 +-
.../compliance/fedramp_20x_ksi_low_aws.py | 46 +++
.../compliance/fedramp_20x_ksi_low_azure.py | 46 +++
.../compliance/fedramp_20x_ksi_low_gcp.py | 46 +++
prowler/CHANGELOG.md | 1 +
.../aws/fedramp_20x_ksi_low_aws.json | 347 +++++++++++++++++
.../azure/fedramp_20x_ksi_low_azure.json | 358 ++++++++++++++++++
.../gcp/fedramp_20x_ksi_low_gcp.json | 293 ++++++++++++++
8 files changed, 1140 insertions(+), 3 deletions(-)
create mode 100644 dashboard/compliance/fedramp_20x_ksi_low_aws.py
create mode 100644 dashboard/compliance/fedramp_20x_ksi_low_azure.py
create mode 100644 dashboard/compliance/fedramp_20x_ksi_low_gcp.py
create mode 100644 prowler/compliance/aws/fedramp_20x_ksi_low_aws.json
create mode 100644 prowler/compliance/azure/fedramp_20x_ksi_low_azure.json
create mode 100644 prowler/compliance/gcp/fedramp_20x_ksi_low_gcp.json
diff --git a/README.md b/README.md
index 83af8dcfb7..25cc8f6abe 100644
--- a/README.md
+++ b/README.md
@@ -82,9 +82,9 @@ prowler dashboard
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | Support | Interface |
|---|---|---|---|---|---|---|
-| AWS | 576 | 82 | 38 | 10 | Official | UI, API, CLI |
-| GCP | 79 | 13 | 12 | 3 | Official | UI, API, CLI |
-| Azure | 162 | 19 | 12 | 4 | Official | UI, API, CLI |
+| AWS | 576 | 82 | 39 | 10 | Official | UI, API, CLI |
+| GCP | 79 | 13 | 13 | 3 | Official | UI, API, CLI |
+| Azure | 162 | 19 | 13 | 4 | Official | UI, API, CLI |
| Kubernetes | 83 | 7 | 5 | 7 | Official | UI, API, CLI |
| GitHub | 17 | 2 | 1 | 0 | Official | Stable | UI, API, CLI |
| M365 | 70 | 7 | 3 | 2 | Official | UI, API, CLI |
diff --git a/dashboard/compliance/fedramp_20x_ksi_low_aws.py b/dashboard/compliance/fedramp_20x_ksi_low_aws.py
new file mode 100644
index 0000000000..5ca220301f
--- /dev/null
+++ b/dashboard/compliance/fedramp_20x_ksi_low_aws.py
@@ -0,0 +1,46 @@
+import warnings
+
+from dashboard.common_methods import get_section_containers_cis
+
+warnings.filterwarnings("ignore")
+
+
+def get_table(data):
+ aux = data[
+ [
+ "REQUIREMENTS_ID",
+ "REQUIREMENTS_DESCRIPTION",
+ "REQUIREMENTS_ATTRIBUTES_SECTION",
+ "CHECKID",
+ "STATUS",
+ "REGION",
+ "ACCOUNTID",
+ "RESOURCEID",
+ ]
+ ].copy()
+
+ # Shorten the long FedRAMP KSI descriptions for better display
+ ksi_short_names = {
+ "A secure cloud service offering will protect user data, control access, and apply zero trust principles": "Identity and Access Management",
+ "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system": "Cloud Native Architecture",
+ "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly": "Change Management",
+ "A secure cloud service provider will continuously educate their employees on cybersecurity measures, testing them regularly": "Cybersecurity Education",
+ "A secure cloud service offering will document, report, and analyze security incidents to ensure regulatory compliance and continuous security improvement": "Incident Reporting",
+ "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes": "Monitoring, Logging, and Auditing",
+ "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured": "Policy and Inventory",
+ "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss": "Recovery Planning",
+ "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources": "Service Configuration",
+ "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources": "Third-Party Information Resources",
+ }
+
+ # Replace long descriptions with short names - use contains for partial matching
+ if not aux.empty:
+ for long_desc, short_name in ksi_short_names.items():
+ mask = aux["REQUIREMENTS_DESCRIPTION"].str.contains(
+ long_desc, na=False, regex=False
+ )
+ aux.loc[mask, "REQUIREMENTS_DESCRIPTION"] = short_name
+
+ return get_section_containers_cis(
+ aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
+ )
diff --git a/dashboard/compliance/fedramp_20x_ksi_low_azure.py b/dashboard/compliance/fedramp_20x_ksi_low_azure.py
new file mode 100644
index 0000000000..5ca220301f
--- /dev/null
+++ b/dashboard/compliance/fedramp_20x_ksi_low_azure.py
@@ -0,0 +1,46 @@
+import warnings
+
+from dashboard.common_methods import get_section_containers_cis
+
+warnings.filterwarnings("ignore")
+
+
+def get_table(data):
+ aux = data[
+ [
+ "REQUIREMENTS_ID",
+ "REQUIREMENTS_DESCRIPTION",
+ "REQUIREMENTS_ATTRIBUTES_SECTION",
+ "CHECKID",
+ "STATUS",
+ "REGION",
+ "ACCOUNTID",
+ "RESOURCEID",
+ ]
+ ].copy()
+
+ # Shorten the long FedRAMP KSI descriptions for better display
+ ksi_short_names = {
+ "A secure cloud service offering will protect user data, control access, and apply zero trust principles": "Identity and Access Management",
+ "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system": "Cloud Native Architecture",
+ "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly": "Change Management",
+ "A secure cloud service provider will continuously educate their employees on cybersecurity measures, testing them regularly": "Cybersecurity Education",
+ "A secure cloud service offering will document, report, and analyze security incidents to ensure regulatory compliance and continuous security improvement": "Incident Reporting",
+ "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes": "Monitoring, Logging, and Auditing",
+ "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured": "Policy and Inventory",
+ "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss": "Recovery Planning",
+ "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources": "Service Configuration",
+ "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources": "Third-Party Information Resources",
+ }
+
+ # Replace long descriptions with short names - use contains for partial matching
+ if not aux.empty:
+ for long_desc, short_name in ksi_short_names.items():
+ mask = aux["REQUIREMENTS_DESCRIPTION"].str.contains(
+ long_desc, na=False, regex=False
+ )
+ aux.loc[mask, "REQUIREMENTS_DESCRIPTION"] = short_name
+
+ return get_section_containers_cis(
+ aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
+ )
diff --git a/dashboard/compliance/fedramp_20x_ksi_low_gcp.py b/dashboard/compliance/fedramp_20x_ksi_low_gcp.py
new file mode 100644
index 0000000000..5ca220301f
--- /dev/null
+++ b/dashboard/compliance/fedramp_20x_ksi_low_gcp.py
@@ -0,0 +1,46 @@
+import warnings
+
+from dashboard.common_methods import get_section_containers_cis
+
+warnings.filterwarnings("ignore")
+
+
+def get_table(data):
+ aux = data[
+ [
+ "REQUIREMENTS_ID",
+ "REQUIREMENTS_DESCRIPTION",
+ "REQUIREMENTS_ATTRIBUTES_SECTION",
+ "CHECKID",
+ "STATUS",
+ "REGION",
+ "ACCOUNTID",
+ "RESOURCEID",
+ ]
+ ].copy()
+
+ # Shorten the long FedRAMP KSI descriptions for better display
+ ksi_short_names = {
+ "A secure cloud service offering will protect user data, control access, and apply zero trust principles": "Identity and Access Management",
+ "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system": "Cloud Native Architecture",
+ "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly": "Change Management",
+ "A secure cloud service provider will continuously educate their employees on cybersecurity measures, testing them regularly": "Cybersecurity Education",
+ "A secure cloud service offering will document, report, and analyze security incidents to ensure regulatory compliance and continuous security improvement": "Incident Reporting",
+ "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes": "Monitoring, Logging, and Auditing",
+ "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured": "Policy and Inventory",
+ "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss": "Recovery Planning",
+ "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources": "Service Configuration",
+ "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources": "Third-Party Information Resources",
+ }
+
+ # Replace long descriptions with short names - use contains for partial matching
+ if not aux.empty:
+ for long_desc, short_name in ksi_short_names.items():
+ mask = aux["REQUIREMENTS_DESCRIPTION"].str.contains(
+ long_desc, na=False, regex=False
+ )
+ aux.loc[mask, "REQUIREMENTS_DESCRIPTION"] = short_name
+
+ return get_section_containers_cis(
+ aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
+ )
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index 286322a155..c6be038f9a 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -19,6 +19,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Add multiple compliance improvements [(#9145)](https://github.com/prowler-cloud/prowler/pull/9145)
- Added validation for invalid checks, services, and categories in `load_checks_to_execute` function [(#8971)](https://github.com/prowler-cloud/prowler/pull/8971)
- NIST CSF 2.0 compliance framework for the AWS provider [(#9185)](https://github.com/prowler-cloud/prowler/pull/9185)
+- Add FedRAMP 20x KSI Low for AWS, Azure and GCP [(#9198)](https://github.com/prowler-cloud/prowler/pull/9198)
### Changed
- Update AWS Direct Connect service metadata to new format [(#8855)](https://github.com/prowler-cloud/prowler/pull/8855)
diff --git a/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json
new file mode 100644
index 0000000000..60afe9d4fd
--- /dev/null
+++ b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json
@@ -0,0 +1,347 @@
+{
+ "Framework": "FedRAMP-20x-KSI-Low",
+ "Name": "FedRAMP 20x Key Security Indicators (KSIs) - Low Impact Level v25.05C",
+ "Version": "25.05C",
+ "Provider": "AWS",
+ "Description": "FedRAMP 20x Key Security Indicators (KSIs) Low Impact Level represent core security indicators for cloud service providers, focusing on automation, continuous monitoring, and cloud-native security principles per FedRAMP 20x Phase One pilot requirements for Low impact systems.",
+ "Requirements": [
+ {
+ "Id": "ksi-cmt",
+ "Name": "KSI-CMT: Change Management",
+ "Description": "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly",
+ "Attributes": [
+ {
+ "ItemId": "ksi-cmt",
+ "Section": "Change Management",
+ "Service": "aws"
+ }
+ ],
+ "Checks": [
+ "cloudtrail_multi_region_enabled",
+ "cloudtrail_log_file_validation_enabled",
+ "cloudtrail_s3_dataevents_read_enabled",
+ "cloudtrail_s3_dataevents_write_enabled",
+ "cloudwatch_changes_to_network_acls_alarm_configured",
+ "cloudwatch_changes_to_network_gateways_alarm_configured",
+ "cloudwatch_changes_to_network_route_tables_alarm_configured",
+ "cloudwatch_changes_to_vpcs_alarm_configured",
+ "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled",
+ "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled",
+ "cloudwatch_log_metric_filter_aws_organizations_changes",
+ "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes",
+ "cloudwatch_log_metric_filter_policy_changes",
+ "cloudwatch_log_metric_filter_security_group_changes",
+ "config_recorder_all_regions_enabled",
+ "ec2_instance_managed_by_ssm",
+ "ec2_instance_older_than_specific_days",
+ "ssm_managed_compliant_patching",
+ "ssm_managed_instance_compliance_association_compliant",
+ "ssm_managed_instance_compliance_patch_compliant"
+ ]
+ },
+ {
+ "Id": "ksi-cna",
+ "Name": "KSI-CNA: Cloud Native Architecture",
+ "Description": "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system",
+ "Attributes": [
+ {
+ "ItemId": "ksi-cna",
+ "Section": "Cloud Native Architecture",
+ "Service": "aws"
+ }
+ ],
+ "Checks": [
+ "autoscaling_group_multiple_az",
+ "autoscaling_group_multiple_instance_types",
+ "autoscaling_group_capacity_rebalance_enabled",
+ "dynamodb_tables_pitr_enabled",
+ "dynamodb_tables_deletion_protection_enabled",
+ "ec2_instance_imdsv2_enabled",
+ "ec2_networkacl_allow_ingress_any_port",
+ "ec2_securitygroup_default_restrict_traffic",
+ "ec2_securitygroup_allow_ingress_from_internet_to_any_port",
+ "eks_cluster_network_policy_enabled",
+ "eks_cluster_not_publicly_accessible",
+ "eks_cluster_private_nodes_enabled",
+ "eks_cluster_uses_a_supported_version",
+ "elb_cross_zone_load_balancing_enabled",
+ "elbv2_alb_multi_az_scheme",
+ "elbv2_waf_acl_attached",
+ "rds_instance_multi_az",
+ "rds_cluster_multi_az",
+ "vpc_subnet_auto_assign_public_ip_disabled",
+ "vpc_default_security_group_restricts_traffic",
+ "vpc_peering_connection_routing_tables_with_least_privilege"
+ ]
+ },
+ {
+ "Id": "ksi-iam",
+ "Name": "KSI-IAM: Identity and Access Management",
+ "Description": "A secure cloud service offering will protect user data, control access, and apply zero trust principles",
+ "Attributes": [
+ {
+ "ItemId": "ksi-iam",
+ "Section": "Identity and Access Management",
+ "Service": "aws"
+ }
+ ],
+ "Checks": [
+ "iam_administrator_access_with_mfa",
+ "iam_aws_attached_policy_no_administrative_privileges",
+ "iam_customer_attached_policy_no_administrative_privileges",
+ "iam_inline_policy_no_administrative_privileges",
+ "iam_no_custom_policy_permissive_role_assumption",
+ "iam_no_root_access_key",
+ "iam_password_policy_expires_passwords_within_90_days_or_less",
+ "iam_password_policy_lowercase",
+ "iam_password_policy_minimum_length_14",
+ "iam_password_policy_number",
+ "iam_password_policy_reuse_24",
+ "iam_password_policy_symbol",
+ "iam_password_policy_uppercase",
+ "iam_policy_attached_only_to_group_or_roles",
+ "iam_policy_no_full_access_to_cloudtrail",
+ "iam_policy_no_full_access_to_kms",
+ "iam_root_hardware_mfa_enabled",
+ "iam_root_mfa_enabled",
+ "iam_rotate_access_key_90_days",
+ "iam_user_accesskey_unused",
+ "iam_user_console_access_unused",
+ "iam_user_hardware_mfa_enabled",
+ "iam_user_mfa_enabled_console_access",
+ "iam_user_two_active_access_key",
+ "organizations_scp_check_deny_regions",
+ "organizations_opt_out_ai_services_policy"
+ ]
+ },
+ {
+ "Id": "ksi-inr",
+ "Name": "KSI-INR: Incident Response",
+ "Description": "A secure cloud service offering will respond to incidents according to FedRAMP requirements and cloud service provider policies",
+ "Attributes": [
+ {
+ "ItemId": "ksi-inr",
+ "Section": "Incident Response",
+ "Service": "aws"
+ }
+ ],
+ "Checks": [
+ "guardduty_centrally_managed",
+ "guardduty_ec2_malware_protection_enabled",
+ "guardduty_eks_audit_log_enabled",
+ "guardduty_eks_protection_enabled",
+ "guardduty_eks_runtime_monitoring_enabled",
+ "guardduty_is_enabled",
+ "guardduty_lambda_protection_enabled",
+ "guardduty_malware_protection_enabled",
+ "guardduty_no_high_severity_findings",
+ "guardduty_rds_protection_enabled",
+ "guardduty_s3_protection_enabled",
+ "inspector2_is_enabled",
+ "inspector2_active_findings_exist",
+ "securityhub_enabled",
+ "sns_topics_kms_encryption_at_rest_enabled"
+ ]
+ },
+ {
+ "Id": "ksi-mla",
+ "Name": "KSI-MLA: Monitoring, Logging, and Auditing",
+ "Description": "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes",
+ "Attributes": [
+ {
+ "ItemId": "ksi-mla",
+ "Section": "Monitoring, Logging, and Auditing",
+ "Service": "aws"
+ }
+ ],
+ "Checks": [
+ "apigateway_restapi_logging_enabled",
+ "cloudtrail_cloudwatch_logging_enabled",
+ "cloudtrail_kms_encryption_enabled",
+ "cloudtrail_log_file_validation_enabled",
+ "cloudtrail_multi_region_enabled",
+ "cloudtrail_s3_dataevents_read_enabled",
+ "cloudtrail_s3_dataevents_write_enabled",
+ "cloudwatch_log_group_kms_encryption_enabled",
+ "cloudwatch_log_group_retention_policy_specific_days_enabled",
+ "ecs_cluster_container_insights_enabled",
+ "eks_cluster_control_plane_audit_logging_enabled",
+ "elb_logging_enabled",
+ "elbv2_logging_enabled",
+ "inspector2_is_enabled",
+ "opensearch_service_domains_cloudwatch_logging_enabled",
+ "rds_instance_enhanced_monitoring_enabled",
+ "rds_instance_integration_cloudwatch_logs",
+ "redshift_cluster_audit_logging",
+ "s3_bucket_server_access_logging_enabled",
+ "vpc_flow_logs_enabled",
+ "wafv2_webacl_logging_enabled"
+ ]
+ },
+ {
+ "Id": "ksi-piy",
+ "Name": "KSI-PIY: Policy and Inventory",
+ "Description": "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured",
+ "Attributes": [
+ {
+ "ItemId": "ksi-piy",
+ "Section": "Policy and Inventory",
+ "Service": "aws"
+ }
+ ],
+ "Checks": [
+ "config_recorder_all_regions_enabled",
+ "config_recorder_using_aws_service_role",
+ "ec2_instance_managed_by_ssm",
+ "organizations_account_part_of_organizations",
+ "organizations_delegated_administrators",
+ "organizations_scp_check_deny_regions",
+ "organizations_tags_policies_enabled_and_attached",
+ "resourceexplorer_indexes_found",
+ "ssm_managed_instance_compliance_association_compliant",
+ "trustedadvisor_premium_support_plan_subscribed"
+ ]
+ },
+ {
+ "Id": "ksi-rpl",
+ "Name": "KSI-RPL: Recovery Planning",
+ "Description": "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss",
+ "Attributes": [
+ {
+ "ItemId": "ksi-rpl",
+ "Section": "Recovery Planning",
+ "Service": "aws"
+ }
+ ],
+ "Checks": [
+ "backup_plans_exist",
+ "backup_reportplans_exist",
+ "backup_vaults_exist",
+ "backup_vaults_encrypted",
+ "backup_recovery_point_encrypted",
+ "backup_recovery_point_manual_deletion_disabled",
+ "backup_recovery_point_minimum_retention_days",
+ "dlm_ebs_snapshot_lifecycle_policy_exists",
+ "dynamodb_tables_pitr_enabled",
+ "dynamodb_tables_deletion_protection_enabled",
+ "efs_have_backup_enabled",
+ "fsx_file_system_copy_tags_to_backups",
+ "rds_instance_backup_enabled",
+ "rds_instance_backup_retention_policy",
+ "rds_instance_deletion_protection",
+ "rds_cluster_deletion_protection",
+ "rds_snapshots_encrypted",
+ "redshift_cluster_automated_snapshot"
+ ]
+ },
+ {
+ "Id": "ksi-svc",
+ "Name": "KSI-SVC: Service Configuration",
+ "Description": "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources",
+ "Attributes": [
+ {
+ "ItemId": "ksi-svc",
+ "Section": "Service Configuration",
+ "Service": "aws"
+ }
+ ],
+ "Checks": [
+ "acm_certificates_expiration_check",
+ "apigateway_restapi_cache_encrypted",
+ "cloudtrail_kms_encryption_enabled",
+ "dax_cluster_encryption_enabled",
+ "dynamodb_table_encryption_enabled",
+ "dynamodb_table_encryption_uses_cmks",
+ "ebs_volume_encryption_enabled",
+ "ec2_ebs_default_encryption",
+ "ec2_instance_ebs_optimized",
+ "efs_encryption_at_rest_enabled",
+ "eks_cluster_envelope_encryption_enabled",
+ "elasticache_redis_cluster_encryption_at_rest_enabled",
+ "elasticache_redis_cluster_encryption_at_transit_enabled",
+ "elbv2_ssl_listeners",
+ "fsx_file_system_encryption_at_rest_enabled",
+ "kinesis_stream_encrypted_at_rest",
+ "kms_cmk_rotation_enabled",
+ "kms_cmk_not_scheduled_for_deletion",
+ "kms_key_not_publicly_accessible",
+ "rds_instance_storage_encrypted",
+ "rds_instance_storage_encrypted_with_cmk",
+ "rds_cluster_storage_encrypted",
+ "redshift_cluster_encryption_at_rest",
+ "redshift_cluster_encryption_in_transit",
+ "s3_bucket_server_side_encryption_enabled",
+ "s3_bucket_default_encryption",
+ "s3_bucket_secure_transport_policy",
+ "sagemaker_notebook_instance_encryption_enabled",
+ "sns_topics_kms_encryption_at_rest_enabled",
+ "sqs_queue_server_side_encryption_enabled"
+ ]
+ },
+ {
+ "Id": "ksi-tpr",
+ "Name": "KSI-TPR: Third-Party Information Resources",
+ "Description": "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources",
+ "Attributes": [
+ {
+ "ItemId": "ksi-tpr",
+ "Section": "Third-Party Information Resources",
+ "Service": "aws"
+ }
+ ],
+ "Checks": [
+ "ecr_registry_scan_images_on_push_enabled",
+ "ecr_repositories_lifecycle_policy_enabled",
+ "ecr_repositories_not_publicly_accessible",
+ "ecr_repositories_scan_on_push_enabled",
+ "ecr_repositories_scan_vulnerabilities_in_latest_image",
+ "ecr_repositories_tag_immutability",
+ "inspector2_active_findings_exist",
+ "inspector2_is_enabled",
+ "awslambda_function_using_supported_runtimes",
+ "ssm_managed_compliant_patching",
+ "trustedadvisor_premium_support_plan_subscribed",
+ "guardduty_no_high_severity_findings"
+ ]
+ },
+ {
+ "Id": "ksi-iam-07",
+ "Name": "KSI-IAM-07: Account Lifecycle Management",
+ "Description": "Securely manage the lifecycle and privileges of all accounts, roles, and groups",
+ "Attributes": [
+ {
+ "ItemId": "ksi-iam-07",
+ "Section": "Identity and Access Management",
+ "Service": "aws"
+ }
+ ],
+ "Checks": [
+ "iam_no_root_access_key",
+ "iam_policy_attached_only_to_group_or_roles",
+ "iam_rotate_access_key_90_days",
+ "iam_user_accesskey_unused",
+ "iam_user_console_access_unused",
+ "organizations_delegated_administrators"
+ ]
+ },
+ {
+ "Id": "ksi-mla-07",
+ "Name": "KSI-MLA-07: Monitoring and Logging Inventory",
+ "Description": "Maintain a list of information resources and event types that will be monitored, logged, and audited",
+ "Attributes": [
+ {
+ "ItemId": "ksi-mla-07",
+ "Section": "Monitoring, Logging, and Auditing",
+ "Service": "aws"
+ }
+ ],
+ "Checks": [
+ "cloudtrail_multi_region_enabled",
+ "cloudwatch_log_group_retention_policy_specific_days_enabled",
+ "config_recorder_all_regions_enabled",
+ "inspector2_is_enabled",
+ "resourceexplorer_indexes_found"
+ ]
+ }
+ ]
+}
diff --git a/prowler/compliance/azure/fedramp_20x_ksi_low_azure.json b/prowler/compliance/azure/fedramp_20x_ksi_low_azure.json
new file mode 100644
index 0000000000..c845f75f60
--- /dev/null
+++ b/prowler/compliance/azure/fedramp_20x_ksi_low_azure.json
@@ -0,0 +1,358 @@
+{
+ "Framework": "FedRAMP-20x-KSI-Low",
+ "Name": "FedRAMP 20x Key Security Indicators (KSIs) - Low Impact Level v25.05C",
+ "Version": "25.05C",
+ "Provider": "Azure",
+ "Description": "FedRAMP 20x Key Security Indicators (KSIs) Low Impact Level represent core security indicators for cloud service providers, focusing on automation, continuous monitoring, and cloud-native security principles per FedRAMP 20x Phase One pilot requirements for Low impact systems.",
+ "Requirements": [
+ {
+ "Id": "ksi-cmt",
+ "Name": "KSI-CMT: Change Management",
+ "Description": "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly",
+ "Attributes": [
+ {
+ "ItemId": "ksi-cmt",
+ "Section": "Change Management",
+ "Service": "azure"
+ }
+ ],
+ "Checks": [
+ "monitor_activity_log_alert_cmk_delete",
+ "monitor_activity_log_alert_create_policy_assignment",
+ "monitor_activity_log_alert_create_update_delete_network_sg",
+ "monitor_activity_log_alert_create_update_delete_network_sg_rule",
+ "monitor_activity_log_alert_create_update_delete_sql_server_fw_rule",
+ "monitor_activity_log_alert_create_update_nsg",
+ "monitor_activity_log_alert_create_update_public_ip_address",
+ "monitor_activity_log_alert_create_update_security_solution",
+ "monitor_activity_log_alert_delete_nsg",
+ "monitor_activity_log_alert_delete_policy_assignment",
+ "monitor_activity_log_alert_delete_public_ip_address",
+ "monitor_activity_log_alert_delete_security_solution",
+ "monitor_log_profile_all_categories",
+ "monitor_log_profile_all_regions",
+ "vm_agent_installed",
+ "vm_antimalware_solution_installed",
+ "vm_endpoint_protection_installed",
+ "vm_guest_configuration_installed",
+ "vm_guest_configuration_with_no_managed_identity",
+ "vm_guest_configuration_with_user_identity"
+ ]
+ },
+ {
+ "Id": "ksi-cna",
+ "Name": "KSI-CNA: Cloud Native Architecture",
+ "Description": "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system",
+ "Attributes": [
+ {
+ "ItemId": "ksi-cna",
+ "Section": "Cloud Native Architecture",
+ "Service": "azure"
+ }
+ ],
+ "Checks": [
+ "aks_clusters_created_with_private_nodes",
+ "aks_clusters_public_access_disabled",
+ "aks_network_policy_enabled",
+ "app_function_vnet_integration_enabled",
+ "app_function_not_publicly_accessible",
+ "containerregistry_not_publicly_accessible",
+ "containerregistry_uses_private_link",
+ "cosmosdb_account_use_private_endpoints",
+ "cosmosdb_account_firewall_use_selected_networks",
+ "databricks_workspace_vnet_injection_enabled",
+ "keyvault_access_only_through_private_endpoints",
+ "keyvault_private_endpoints",
+ "network_bastion_host_exists",
+ "network_flow_logs_enabled",
+ "network_security_group_not_empty",
+ "network_sg_ssh_access_restricted",
+ "network_sg_rdp_access_restricted",
+ "network_sg_open_all_ports_to_any_source",
+ "network_watcher_enabled",
+ "postgresql_flexible_server_public_network_access_disabled",
+ "sqlserver_public_network_access_disabled",
+ "storage_default_network_access_rule_set_to_deny",
+ "vm_availability_zones_enabled",
+ "vm_availability_set_deployed"
+ ]
+ },
+ {
+ "Id": "ksi-iam",
+ "Name": "KSI-IAM: Identity and Access Management",
+ "Description": "A secure cloud service offering will protect user data, control access, and apply zero trust principles",
+ "Attributes": [
+ {
+ "ItemId": "ksi-iam",
+ "Section": "Identity and Access Management",
+ "Service": "azure"
+ }
+ ],
+ "Checks": [
+ "entra_conditional_access_policy_require_mfa_for_management_api",
+ "entra_global_admin_in_less_than_five_users",
+ "entra_non_privileged_user_has_mfa",
+ "entra_policy_default_users_cannot_create_security_groups",
+ "entra_policy_ensure_default_user_cannot_create_apps",
+ "entra_policy_ensure_default_user_cannot_create_tenants",
+ "entra_policy_guest_invite_only_for_admin_roles",
+ "entra_policy_guest_users_access_restrictions",
+ "entra_policy_restricts_user_consent_for_apps",
+ "entra_policy_user_consent_for_verified_apps",
+ "entra_privileged_user_has_mfa",
+ "entra_security_defaults_enabled",
+ "entra_trusted_named_locations_exists",
+ "entra_user_with_vm_access_has_mfa",
+ "entra_users_cannot_create_microsoft_365_groups",
+ "iam_custom_role_has_permissions_to_administer_resource_locks",
+ "iam_role_user_access_admin_restricted",
+ "iam_subscription_roles_owner_custom_not_created",
+ "keyvault_rbac_enabled",
+ "app_function_identity_is_configured",
+ "app_function_identity_without_admin_privileges",
+ "app_ensure_auth_is_set_up",
+ "app_register_with_identity",
+ "vm_managed_identity_enabled"
+ ]
+ },
+ {
+ "Id": "ksi-inr",
+ "Name": "KSI-INR: Incident Response",
+ "Description": "A secure cloud service offering will respond to incidents according to FedRAMP requirements and cloud service provider policies",
+ "Attributes": [
+ {
+ "ItemId": "ksi-inr",
+ "Section": "Incident Response",
+ "Service": "azure"
+ }
+ ],
+ "Checks": [
+ "defender_attack_path_notifications_properly_configured",
+ "defender_ensure_notify_alerts_severity_is_high",
+ "defender_ensure_notify_emails_to_owners",
+ "defender_additional_email_configured_with_a_security_contact",
+ "defender_container_images_resolved_vulnerabilities",
+ "defender_container_images_scan_enabled",
+ "defender_ensure_defender_for_app_services_is_on",
+ "defender_ensure_defender_for_arm_is_on",
+ "defender_ensure_defender_for_azure_sql_databases_is_on",
+ "defender_ensure_defender_for_containers_is_on",
+ "defender_ensure_defender_for_cosmosdb_is_on",
+ "defender_ensure_defender_for_databases_is_on",
+ "defender_ensure_defender_for_dns_is_on",
+ "defender_ensure_defender_for_keyvault_is_on",
+ "defender_ensure_defender_for_os_relational_databases_is_on",
+ "defender_ensure_defender_for_server_is_on",
+ "defender_ensure_defender_for_sql_servers_is_on",
+ "defender_ensure_defender_for_storage_is_on",
+ "defender_ensure_iot_hub_defender_is_on",
+ "defender_ensure_wdatp_is_enabled"
+ ]
+ },
+ {
+ "Id": "ksi-mla",
+ "Name": "KSI-MLA: Monitoring, Logging, and Auditing",
+ "Description": "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes",
+ "Attributes": [
+ {
+ "ItemId": "ksi-mla",
+ "Section": "Monitoring, Logging, and Auditing",
+ "Service": "azure"
+ }
+ ],
+ "Checks": [
+ "app_function_application_insights_enabled",
+ "app_http_logs_enabled",
+ "appinsights_ensure_is_configured",
+ "defender_auto_provisioning_log_analytics_agent_vms_on",
+ "defender_auto_provisioning_vulnerabilty_assessments_machines_on",
+ "keyvault_logging_enabled",
+ "monitor_activity_log_retention_policy_set",
+ "monitor_diagnostic_logs_categories",
+ "monitor_diagnostic_setting_deployed_for_all_resources",
+ "monitor_diagnostic_settings_captures_proper_categories",
+ "monitor_log_profile_all_categories",
+ "monitor_log_profile_all_regions",
+ "monitor_log_profile_captures_all_activities",
+ "monitor_log_profile_retention_policy_at_least_365",
+ "network_flow_logs_enabled",
+ "network_flow_log_retention_policy_at_least_90",
+ "network_watcher_enabled",
+ "postgresql_flexible_server_audit_logs_enabled",
+ "postgresql_flexible_server_log_checkpoints_enabled",
+ "postgresql_flexible_server_log_connections_enabled",
+ "postgresql_flexible_server_log_disconnections_enabled",
+ "sqlserver_auditing_on",
+ "sqlserver_auditing_retention_90_days",
+ "storage_storage_account_logging_queue_read_write_delete_enabled"
+ ]
+ },
+ {
+ "Id": "ksi-piy",
+ "Name": "KSI-PIY: Policy and Inventory",
+ "Description": "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured",
+ "Attributes": [
+ {
+ "ItemId": "ksi-piy",
+ "Section": "Policy and Inventory",
+ "Service": "azure"
+ }
+ ],
+ "Checks": [
+ "policy_ensure_asc_for_aks_is_enabled",
+ "policy_ensure_asc_for_app_services_is_enabled",
+ "policy_ensure_asc_for_azure_sql_is_enabled",
+ "policy_ensure_asc_for_key_vault_is_enabled",
+ "policy_ensure_asc_for_servers_is_enabled",
+ "policy_ensure_asc_for_sql_servers_is_enabled",
+ "policy_ensure_asc_for_storage_is_enabled",
+ "policy_ensure_allowed_extensions_are_installed",
+ "policy_ensure_allowed_locations_is_enabled",
+ "policy_ensure_allowed_resource_types_is_enabled",
+ "policy_ensure_audit_diagnostic_log_enabled_for_all_services",
+ "policy_ensure_not_allowed_resource_types_is_enabled",
+ "vm_guest_configuration_installed",
+ "vm_guest_configuration_with_no_managed_identity",
+ "vm_guest_configuration_with_user_identity"
+ ]
+ },
+ {
+ "Id": "ksi-rpl",
+ "Name": "KSI-RPL: Recovery Planning",
+ "Description": "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss",
+ "Attributes": [
+ {
+ "ItemId": "ksi-rpl",
+ "Section": "Recovery Planning",
+ "Service": "azure"
+ }
+ ],
+ "Checks": [
+ "mysql_flexible_server_geo_redundant_backup_enabled",
+ "mysql_flexible_server_retain_backup_35_days",
+ "postgresql_flexible_server_geo_redundant_backup_enabled",
+ "postgresql_flexible_server_backup_retention_period_35_days",
+ "recovery_services_vault_uses_private_link",
+ "recovery_services_vault_uses_private_link_for_backup",
+ "sqlserver_database_long_term_geo_redundant_backup",
+ "sqlserver_database_retention_policy_exceeds_90_days",
+ "storage_default_storage_account_encrypted_with_cmk_not_stored_in_storage_account",
+ "storage_geo_redundant_enabled",
+ "storage_infrastructure_encryption_is_enabled",
+ "storage_soft_delete_containers_enabled",
+ "storage_soft_delete_enabled",
+ "vm_backup_enabled",
+ "vm_sufficient_daily_backup_retention_period"
+ ]
+ },
+ {
+ "Id": "ksi-svc",
+ "Name": "KSI-SVC: Service Configuration",
+ "Description": "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources",
+ "Attributes": [
+ {
+ "ItemId": "ksi-svc",
+ "Section": "Service Configuration",
+ "Service": "azure"
+ }
+ ],
+ "Checks": [
+ "app_client_certificates_on",
+ "app_ensure_http_is_redirected_to_https",
+ "app_minimum_tls_version_12",
+ "containerregistry_admin_user_disabled",
+ "cosmosdb_account_use_aad_and_rbac",
+ "databricks_workspace_cmk_encryption_enabled",
+ "keyvault_key_expiration_set_in_non_rbac",
+ "keyvault_key_rotation_enabled",
+ "keyvault_non_rbac_secret_expiration_set",
+ "mysql_flexible_server_encrypted_at_rest_using_cmk",
+ "mysql_flexible_server_encrypted_in_transit",
+ "mysql_flexible_server_minimum_tls_version_tls12",
+ "postgresql_flexible_server_encrypted_at_rest_using_cmk",
+ "postgresql_flexible_server_encrypted_in_transit",
+ "postgresql_flexible_server_minimum_tls_version_tls12",
+ "sqlserver_advanced_data_security_enabled",
+ "sqlserver_database_encryption_with_cmk",
+ "sqlserver_database_tde_encryption_enabled",
+ "sqlserver_minimum_tls_version_12",
+ "storage_secure_transfer_required_enabled",
+ "storage_default_storage_account_encrypted_with_cmk",
+ "storage_infrastructure_encryption_is_enabled",
+ "storage_storage_account_encrypted_with_cmk",
+ "storage_storage_account_minimum_tls_version_tls12",
+ "vm_encrypted_at_host",
+ "vm_data_disks_encrypted_with_cmk",
+ "vm_managed_disks_encrypted_with_cmk",
+ "vm_os_disk_are_encrypted_with_cmk",
+ "vm_temporary_disks_and_cache_encrypted"
+ ]
+ },
+ {
+ "Id": "ksi-tpr",
+ "Name": "KSI-TPR: Third-Party Information Resources",
+ "Description": "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources",
+ "Attributes": [
+ {
+ "ItemId": "ksi-tpr",
+ "Section": "Third-Party Information Resources",
+ "Service": "azure"
+ }
+ ],
+ "Checks": [
+ "app_ensure_java_version_is_latest",
+ "app_ensure_php_version_is_latest",
+ "app_ensure_python_version_is_latest",
+ "app_function_latest_runtime_version",
+ "defender_container_images_resolved_vulnerabilities",
+ "defender_container_images_scan_enabled",
+ "defender_ensure_system_updates_are_applied",
+ "vm_agent_installed",
+ "vm_antimalware_solution_installed",
+ "vm_endpoint_protection_installed",
+ "vm_os_update_system_updates",
+ "vm_security_patch_assessment"
+ ]
+ },
+ {
+ "Id": "ksi-iam-07",
+ "Name": "KSI-IAM-07: Account Lifecycle Management",
+ "Description": "Securely manage the lifecycle and privileges of all accounts, roles, and groups",
+ "Attributes": [
+ {
+ "ItemId": "ksi-iam-07",
+ "Section": "Identity and Access Management",
+ "Service": "azure"
+ }
+ ],
+ "Checks": [
+ "entra_non_privileged_user_has_mfa",
+ "entra_privileged_user_has_mfa",
+ "entra_user_with_vm_access_has_mfa",
+ "iam_custom_role_has_permissions_to_administer_resource_locks",
+ "iam_role_user_access_admin_restricted",
+ "app_function_identity_is_configured",
+ "vm_managed_identity_enabled"
+ ]
+ },
+ {
+ "Id": "ksi-mla-07",
+ "Name": "KSI-MLA-07: Monitoring and Logging Inventory",
+ "Description": "Maintain a list of information resources and event types that will be monitored, logged, and audited",
+ "Attributes": [
+ {
+ "ItemId": "ksi-mla-07",
+ "Section": "Monitoring, Logging, and Auditing",
+ "Service": "azure"
+ }
+ ],
+ "Checks": [
+ "monitor_log_profile_all_categories",
+ "monitor_log_profile_all_regions",
+ "monitor_log_profile_captures_all_activities",
+ "monitor_diagnostic_setting_deployed_for_all_resources",
+ "network_watcher_enabled"
+ ]
+ }
+ ]
+}
diff --git a/prowler/compliance/gcp/fedramp_20x_ksi_low_gcp.json b/prowler/compliance/gcp/fedramp_20x_ksi_low_gcp.json
new file mode 100644
index 0000000000..420601d810
--- /dev/null
+++ b/prowler/compliance/gcp/fedramp_20x_ksi_low_gcp.json
@@ -0,0 +1,293 @@
+{
+ "Framework": "FedRAMP-20x-KSI-Low",
+ "Name": "FedRAMP 20x Key Security Indicators (KSIs) - Low Impact Level v25.05C",
+ "Version": "25.05C",
+ "Provider": "GCP",
+ "Description": "FedRAMP 20x Key Security Indicators (KSIs) Low Impact Level represent core security indicators for cloud service providers, focusing on automation, continuous monitoring, and cloud-native security principles per FedRAMP 20x Phase One pilot requirements for Low impact systems.",
+ "Requirements": [
+ {
+ "Id": "ksi-cmt",
+ "Name": "KSI-CMT: Change Management",
+ "Description": "A secure cloud service provider will ensure that all system changes are properly documented and configuration baselines are updated accordingly",
+ "Attributes": [
+ {
+ "ItemId": "ksi-cmt",
+ "Section": "Change Management",
+ "Service": "gcp"
+ }
+ ],
+ "Checks": [
+ "iam_audit_logs_enabled",
+ "iam_cloud_asset_inventory_enabled",
+ "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled",
+ "compute_instance_serial_ports_in_use",
+ "compute_project_os_login_enabled"
+ ]
+ },
+ {
+ "Id": "ksi-cna",
+ "Name": "KSI-CNA: Cloud Native Architecture",
+ "Description": "A secure cloud service offering will use cloud native architecture and design principles to enforce and enhance the Confidentiality, Integrity and Availability of the system",
+ "Attributes": [
+ {
+ "ItemId": "ksi-cna",
+ "Section": "Cloud Native Architecture",
+ "Service": "gcp"
+ }
+ ],
+ "Checks": [
+ "cloudsql_instance_private_ip_assignment",
+ "cloudsql_instance_public_access",
+ "cloudsql_instance_public_ip",
+ "cloudstorage_bucket_uniform_bucket_level_access",
+ "compute_firewall_rdp_access_from_the_internet_allowed",
+ "compute_firewall_ssh_access_from_the_internet_allowed",
+ "compute_instance_block_project_wide_ssh_keys_disabled",
+ "compute_instance_confidential_computing_enabled",
+ "compute_instance_ip_forwarding_is_enabled",
+ "compute_instance_public_ip",
+ "compute_instance_shielded_vm_enabled",
+ "compute_loadbalancer_logging_enabled",
+ "compute_network_default_in_use",
+ "compute_network_dns_logging_enabled",
+ "compute_network_not_legacy",
+ "compute_subnet_flow_logs_enabled",
+ "gke_cluster_no_default_service_account"
+ ]
+ },
+ {
+ "Id": "ksi-iam",
+ "Name": "KSI-IAM: Identity and Access Management",
+ "Description": "A secure cloud service offering will protect user data, control access, and apply zero trust principles",
+ "Attributes": [
+ {
+ "ItemId": "ksi-iam",
+ "Section": "Identity and Access Management",
+ "Service": "gcp"
+ }
+ ],
+ "Checks": [
+ "apikeys_api_restrictions_configured",
+ "apikeys_key_exists",
+ "apikeys_key_rotated_in_90_days",
+ "compute_instance_default_service_account_in_use",
+ "compute_instance_default_service_account_in_use_with_full_api_access",
+ "iam_no_service_roles_at_project_level",
+ "iam_role_kms_enforce_separation_of_duties",
+ "iam_role_sa_enforce_separation_of_duties",
+ "iam_sa_no_administrative_privileges",
+ "iam_sa_no_user_managed_keys",
+ "iam_sa_user_managed_key_rotate_90_days",
+ "iam_sa_user_managed_key_unused",
+ "iam_service_account_unused"
+ ]
+ },
+ {
+ "Id": "ksi-inr",
+ "Name": "KSI-INR: Incident Response",
+ "Description": "A secure cloud service offering will respond to incidents according to FedRAMP requirements and cloud service provider policies",
+ "Attributes": [
+ {
+ "ItemId": "ksi-inr",
+ "Section": "Incident Response",
+ "Service": "gcp"
+ }
+ ],
+ "Checks": [
+ "iam_organization_essential_contacts_configured",
+ "iam_account_access_approval_enabled",
+ "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled"
+ ]
+ },
+ {
+ "Id": "ksi-mla",
+ "Name": "KSI-MLA: Monitoring, Logging, and Auditing",
+ "Description": "A secure cloud service offering will monitor, log, and audit all important events, activity, and changes",
+ "Attributes": [
+ {
+ "ItemId": "ksi-mla",
+ "Section": "Monitoring, Logging, and Auditing",
+ "Service": "gcp"
+ }
+ ],
+ "Checks": [
+ "cloudsql_instance_postgres_enable_pgaudit_flag",
+ "cloudsql_instance_postgres_log_connections_flag",
+ "cloudsql_instance_postgres_log_disconnections_flag",
+ "cloudsql_instance_postgres_log_error_verbosity_flag",
+ "cloudsql_instance_postgres_log_min_duration_statement_flag",
+ "cloudsql_instance_postgres_log_min_error_statement_flag",
+ "cloudsql_instance_postgres_log_min_messages_flag",
+ "cloudsql_instance_postgres_log_statement_flag",
+ "cloudsql_instance_sqlserver_trace_flag",
+ "cloudstorage_bucket_log_retention_policy_lock",
+ "compute_loadbalancer_logging_enabled",
+ "compute_network_dns_logging_enabled",
+ "compute_subnet_flow_logs_enabled",
+ "iam_audit_logs_enabled",
+ "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled",
+ "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled",
+ "logging_sink_created"
+ ]
+ },
+ {
+ "Id": "ksi-piy",
+ "Name": "KSI-PIY: Policy and Inventory",
+ "Description": "A secure cloud service offering will have intentional, organized, universal guidance for how every information resource, including personnel, is secured",
+ "Attributes": [
+ {
+ "ItemId": "ksi-piy",
+ "Section": "Policy and Inventory",
+ "Service": "gcp"
+ }
+ ],
+ "Checks": [
+ "iam_cloud_asset_inventory_enabled",
+ "iam_organization_essential_contacts_configured",
+ "iam_audit_logs_enabled",
+ "compute_project_os_login_enabled",
+ "compute_instance_serial_ports_in_use",
+ "compute_instance_block_project_wide_ssh_keys_disabled",
+ "logging_sink_created"
+ ]
+ },
+ {
+ "Id": "ksi-rpl",
+ "Name": "KSI-RPL: Recovery Planning",
+ "Description": "A secure cloud service offering will define, maintain, and test incident response plan(s) and recovery capabilities to ensure minimal service disruption and data loss",
+ "Attributes": [
+ {
+ "ItemId": "ksi-rpl",
+ "Section": "Recovery Planning",
+ "Service": "gcp"
+ }
+ ],
+ "Checks": [
+ "cloudsql_instance_automated_backups",
+ "cloudstorage_bucket_log_retention_policy_lock",
+ "cloudstorage_bucket_versioning_enabled",
+ "cloudstorage_bucket_lifecycle_management_enabled"
+ ]
+ },
+ {
+ "Id": "ksi-svc",
+ "Name": "KSI-SVC: Service Configuration",
+ "Description": "A secure cloud service offering will follow FedRAMP encryption policies, continuously verify information resource integrity, and restrict access to third-party information resources",
+ "Attributes": [
+ {
+ "ItemId": "ksi-svc",
+ "Section": "Service Configuration",
+ "Service": "gcp"
+ }
+ ],
+ "Checks": [
+ "bigquery_dataset_cmk_encryption",
+ "bigquery_table_cmk_encryption",
+ "cloudsql_instance_mysql_local_infile_flag",
+ "cloudsql_instance_mysql_skip_show_database_flag",
+ "cloudsql_instance_postgres_enable_pgaudit_flag",
+ "cloudsql_instance_postgres_log_connections_flag",
+ "cloudsql_instance_postgres_log_disconnections_flag",
+ "cloudsql_instance_postgres_log_error_verbosity_flag",
+ "cloudsql_instance_postgres_log_min_duration_statement_flag",
+ "cloudsql_instance_postgres_log_min_error_statement_flag",
+ "cloudsql_instance_postgres_log_min_messages_flag",
+ "cloudsql_instance_postgres_log_statement_flag",
+ "cloudsql_instance_sqlserver_contained_database_authentication_flag",
+ "cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag",
+ "cloudsql_instance_sqlserver_external_scripts_enabled_flag",
+ "cloudsql_instance_sqlserver_remote_access_flag",
+ "cloudsql_instance_sqlserver_trace_flag",
+ "cloudsql_instance_sqlserver_user_connections_flag",
+ "cloudsql_instance_sqlserver_user_options_flag",
+ "cloudsql_instance_ssl_connections",
+ "compute_instance_encryption_with_csek_enabled",
+ "compute_instance_shielded_vm_enabled",
+ "dataproc_encrypted_with_cmks_disabled",
+ "dns_dnssec_disabled",
+ "dns_rsasha1_in_use_to_key_sign_in_dnssec",
+ "dns_rsasha1_in_use_to_zone_sign_in_dnssec",
+ "kms_key_not_publicly_accessible",
+ "kms_key_rotation_enabled"
+ ]
+ },
+ {
+ "Id": "ksi-tpr",
+ "Name": "KSI-TPR: Third-Party Information Resources",
+ "Description": "A secure cloud service offering will understand, monitor, and manage supply chain risks from third-party information resources",
+ "Attributes": [
+ {
+ "ItemId": "ksi-tpr",
+ "Section": "Third-Party Information Resources",
+ "Service": "gcp"
+ }
+ ],
+ "Checks": [
+ "artifacts_container_analysis_enabled",
+ "gcr_container_scanning_enabled",
+ "compute_public_address_shodan",
+ "cloudsql_instance_automated_backups",
+ "iam_sa_user_managed_key_rotate_90_days",
+ "iam_service_account_unused"
+ ]
+ },
+ {
+ "Id": "ksi-iam-07",
+ "Name": "KSI-IAM-07: Account Lifecycle Management",
+ "Description": "Securely manage the lifecycle and privileges of all accounts, roles, and groups",
+ "Attributes": [
+ {
+ "ItemId": "ksi-iam-07",
+ "Section": "Identity and Access Management",
+ "Service": "gcp"
+ }
+ ],
+ "Checks": [
+ "apikeys_key_rotated_in_90_days",
+ "iam_sa_user_managed_key_rotate_90_days",
+ "iam_sa_user_managed_key_unused",
+ "iam_service_account_unused",
+ "compute_instance_default_service_account_in_use"
+ ]
+ },
+ {
+ "Id": "ksi-mla-07",
+ "Name": "KSI-MLA-07: Monitoring and Logging Inventory",
+ "Description": "Maintain a list of information resources and event types that will be monitored, logged, and audited",
+ "Attributes": [
+ {
+ "ItemId": "ksi-mla-07",
+ "Section": "Monitoring, Logging, and Auditing",
+ "Service": "gcp"
+ }
+ ],
+ "Checks": [
+ "iam_audit_logs_enabled",
+ "iam_cloud_asset_inventory_enabled",
+ "logging_sink_created",
+ "compute_subnet_flow_logs_enabled",
+ "compute_network_dns_logging_enabled"
+ ]
+ }
+ ]
+}
From 521afab4aa66681d310cef1ff4be2aa964ad6e44 Mon Sep 17 00:00:00 2001
From: Prowler Bot
Date: Mon, 10 Nov 2025 15:37:18 +0100
Subject: [PATCH 04/23] feat(aws): Update regions for AWS services (#9194)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
---
.../providers/aws/aws_regions_by_service.json | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json
index 6969414824..c6217b0c60 100644
--- a/prowler/providers/aws/aws_regions_by_service.json
+++ b/prowler/providers/aws/aws_regions_by_service.json
@@ -1555,6 +1555,7 @@
"aws": [
"af-south-1",
"ap-east-1",
+ "ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
@@ -1565,6 +1566,8 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
+ "ap-southeast-6",
+ "ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -1578,6 +1581,7 @@
"il-central-1",
"me-central-1",
"me-south-1",
+ "mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -4584,8 +4588,10 @@
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
+ "eu-south-1",
"eu-west-1",
"eu-west-2",
+ "eu-west-3",
"us-east-1",
"us-east-2",
"us-west-2"
@@ -7261,6 +7267,7 @@
"eu-west-1",
"eu-west-2",
"eu-west-3",
+ "me-central-1",
"me-south-1",
"sa-east-1",
"us-east-1",
@@ -7953,6 +7960,7 @@
"aws": [
"af-south-1",
"ap-east-1",
+ "ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
@@ -7963,6 +7971,8 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
+ "ap-southeast-6",
+ "ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
@@ -7976,6 +7986,7 @@
"il-central-1",
"me-central-1",
"me-south-1",
+ "mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
@@ -9799,6 +9810,20 @@
]
}
},
+ "rtbfabric": {
+ "regions": {
+ "aws": [
+ "ap-northeast-1",
+ "ap-southeast-1",
+ "eu-central-1",
+ "eu-west-1",
+ "us-east-1",
+ "us-west-2"
+ ],
+ "aws-cn": [],
+ "aws-us-gov": []
+ }
+ },
"rum": {
"regions": {
"aws": [
From be0b8bba0de2425980d9dbfe677ee5d3e839d734 Mon Sep 17 00:00:00 2001
From: Sergio Garcia
Date: Mon, 10 Nov 2025 10:15:54 -0500
Subject: [PATCH 05/23] fix(html): rename `get_oci_assessment_summary` (#9200)
---
prowler/CHANGELOG.md | 1 +
prowler/lib/outputs/html/html.py | 16 +++++++++-------
2 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index c6be038f9a..e3823d9fa5 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -51,6 +51,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Check `check_name` has no `resource_name` error for GCP provider [(#9169)](https://github.com/prowler-cloud/prowler/pull/9169)
- Depth Truncation and parsing error in PowerShell queries [(#9181)](https://github.com/prowler-cloud/prowler/pull/9181)
- Fix M365 Teams `--sp-env-auth` connection error and enhanced timeout logging [(#9191)](https://github.com/prowler-cloud/prowler/pull/9191)
+- Rename `get_oci_assessment_summary` to `get_oraclecloud_assessment_summary` in HTML output [(#9200)](https://github.com/prowler-cloud/prowler/pull/9200)
---
diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py
index d8a390bd1b..9295cc4abb 100644
--- a/prowler/lib/outputs/html/html.py
+++ b/prowler/lib/outputs/html/html.py
@@ -974,18 +974,20 @@ class HTML(Output):
return ""
@staticmethod
- def get_oci_assessment_summary(provider: Provider) -> str:
+ def get_oraclecloud_assessment_summary(provider: Provider) -> str:
"""
- get_oci_assessment_summary gets the HTML assessment summary for the OCI provider
+ get_oraclecloud_assessment_summary gets the HTML assessment summary for the OracleCloud provider
Args:
- provider (Provider): the OCI provider object
+ provider (Provider): the OracleCloud provider object
Returns:
- str: HTML assessment summary for the OCI provider
+ str: HTML assessment summary for the OracleCloud provider
"""
try:
profile = getattr(provider.session, "profile", "default")
+ if profile is None:
+ profile = "instance-principal"
tenancy_name = getattr(provider.identity, "tenancy_name", "unknown")
tenancy_id = getattr(provider.identity, "tenancy_id", "unknown")
@@ -993,11 +995,11 @@ class HTML(Output):
-
- OCI Tenancy: {tenancy_name if tenancy_name != "unknown" else tenancy_id}
+ OracleCloud Tenancy: {tenancy_name if tenancy_name != "unknown" else tenancy_id}
@@ -1005,7 +1007,7 @@ class HTML(Output):
-
From 7c339ed9e496ebbd8a7c3f455e1d8831a69c5fb9 Mon Sep 17 00:00:00 2001
From: Sergio Garcia
Date: Mon, 10 Nov 2025 13:39:24 -0500
Subject: [PATCH 06/23] docs(mutelist): fix misleading docstrings about tag and
exception logic (#9205)
---
docs/user-guide/cli/tutorials/mutelist.mdx | 113 +++++++++++++++++++--
prowler/lib/mutelist/mutelist.py | 22 ++--
2 files changed, 118 insertions(+), 17 deletions(-)
diff --git a/docs/user-guide/cli/tutorials/mutelist.mdx b/docs/user-guide/cli/tutorials/mutelist.mdx
index 4bb7524fa6..b5591edf18 100644
--- a/docs/user-guide/cli/tutorials/mutelist.mdx
+++ b/docs/user-guide/cli/tutorials/mutelist.mdx
@@ -19,9 +19,39 @@ The Mutelist option works in combination with other filtering mechanisms and mod
## How the Mutelist Works
-The **Mutelist** uses both "AND" and "OR" logic to determine which resources, checks, regions, and tags should be muted. For each check, the Mutelist evaluates whether the account, region, and resource match the specified criteria using "AND" logic. If tags are specified, the Mutelist can apply either "AND" or "OR" logic.
+The **Mutelist** uses **AND logic** to evaluate whether a finding should be muted. For a finding to be muted, **ALL** of the following conditions must match:
-If any of the criteria do not match, the check is not muted.
+- **Account** matches (exact match or `*`)
+- **Check** matches (exact match, regex pattern, or `*`)
+- **Region** matches (exact match, regex pattern, or `*`)
+- **Resource** matches (exact match, regex pattern, or `*`)
+- **Tags** match (if specified)
+
+If **any** of these criteria do not match, the finding is **not muted**.
+
+### Tag Matching Logic
+
+Tags have special matching behavior:
+
+- **Multiple tags in the list = AND logic**: ALL tags must be present on the resource
+ ```yaml
+ Tags:
+ - "environment=dev"
+ - "team=backend" # BOTH tags required
+ ```
+
+- **Regex alternation within a single tag = OR logic**: Use the pipe operator `|` for OR
+ ```yaml
+ Tags:
+ - "environment=dev|environment=stg" # Matches EITHER dev OR stg
+ ```
+
+- **Complex tag patterns**: Combine AND and OR using regex
+ ```yaml
+ Tags:
+ - "team=backend" # Required
+ - "environment=dev|environment=stg" # AND (dev OR stg)
+ ```
Remember that mutelist can be used with regular expressions.
@@ -40,9 +70,10 @@ The Mutelist file uses the [YAML](https://en.wikipedia.org/wiki/YAML) format wit
```yaml
### Account, Check and/or Region can be * to apply for all the cases.
### Resources and tags are lists that can have either Regex or Keywords.
-### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together.
-### Use an alternation Regex to match one of multiple tags with "ORed" logic.
-### For each check you can except Accounts, Regions, Resources and/or Tags.
+### Multiple tags in the list are "ANDed" together (ALL must match).
+### Use regex alternation (|) within a single tag for "OR" logic (e.g., "env=dev|env=stg").
+### For each check you can use Exceptions to unmute specific Accounts, Regions, Resources and/or Tags.
+### All conditions (Account, Check, Region, Resource, Tags) are ANDed together.
########################### MUTELIST EXAMPLE ###########################
Mutelist:
Accounts:
@@ -148,11 +179,11 @@ Mutelist:
| Field| Description| Logic
|----------|----------|----------
-| `account_id`| Use `*` to apply the mutelist to all accounts.| `ANDed`
-| `check_name`| The name of the Prowler check. Use `*` to apply the mutelist to all checks, or `service_*` to apply it to all service's checks.| `ANDed`
-| `region`| The region identifier. Use `*` to apply the mutelist to all regions.| `ANDed`
-| `resource`| The resource identifier. Use `*` to apply the mutelist to all resources.| `ANDed`
-| `tag`| The tag value.| `ORed`
+| `account_id`| Use `*` to apply the mutelist to all accounts. Supports exact match or wildcard.| `AND` (with other fields)
+| `check_name`| The name of the Prowler check. Use `*` to apply the mutelist to all checks, or `service_*` to apply it to all service's checks. Supports regex patterns.| `AND` (with other fields)
+| `region`| The region identifier. Use `*` to apply the mutelist to all regions. Supports regex patterns.| `AND` (with other fields)
+| `resource`| The resource identifier. Use `*` to apply the mutelist to all resources. Supports regex patterns.| `AND` (with other fields)
+| `tags`| List of tag patterns in `key=value` format. **Multiple tags = AND** (all must match). **Regex alternation within single tag = OR** (use `tag1\|tag2`).| `AND` between tags, `OR` within regex
### Description
@@ -173,6 +204,68 @@ Replace `` with the appropriate provider name.
- The Mutelist can be used in combination with other Prowler options, such as the `--service` or `--checks` option, to further customize the scanning process.
- Make sure to review and update the Mutelist regularly to ensure it reflects the desired exclusions and remains up to date with your infrastructure.
+## Current Limitations and Workarounds
+
+### Limitation: No OR Logic Between Different Rule Sets
+
+The current Mutelist schema **does not support OR logic** between different condition sets. Each check can have only **one rule object**, and all conditions are **ANDed** together.
+
+**Example of unsupported scenario:**
+```yaml
+# ❌ INVALID: Cannot have multiple rule blocks for the same check
+Accounts:
+ "*":
+ Checks:
+ "*": # Rule 1
+ Regions: ["eu-west-1", "us-west-2"]
+ Resources: ["*"]
+ "*": # Rule 2 - This will OVERWRITE Rule 1 (YAML duplicate key)
+ Regions: ["us-east-1"]
+ Tags: ["environment=dev"]
+```
+
+**Workaround: Use multiple scans with different mutelists**
+
+For complex scenarios requiring OR logic, run separate scans:
+
+```bash
+# Scan 1: Mute findings in non-critical regions
+prowler aws --mutelist-file mutelist_noncritical.yaml
+
+# Scan 2: Mute dev/stg in critical regions
+prowler aws --mutelist-file mutelist_critical.yaml --regions us-east-1,sa-east-1
+```
+
+Then merge the outputs in your reporting pipeline.
+
+### Limitation: Cannot Negate Regions
+
+You cannot express "all regions **except** X and Y". You must explicitly list all regions you want to mute.
+
+**Workaround:**
+```yaml
+# Must enumerate all unwanted regions
+Accounts:
+ "*":
+ Checks:
+ "*":
+ Regions:
+ - "af-south-1"
+ - "ap-east-1"
+ # ... list all regions EXCEPT the ones you want to monitor
+ Resources: ["*"]
+```
+
+### Best Practices
+
+1. **Use regex patterns for flexibility**: Instead of listing multiple resources, use regex patterns like `"dev-.*"` or `"test-instance-[0-9]+"`
+
+2. **Combine tag OR logic with regex**: Use `"environment=dev|environment=stg|environment=test"` instead of multiple tag entries
+
+3. **Be specific with exceptions**: Use the `Exceptions` field to unmute specific resources within a broader muting rule
+
+4. **Test your mutelist**: Run Prowler with `--output-modes json` and verify that the expected findings are muted
+
## AWS Mutelist
### Muting specific AWS regions
diff --git a/prowler/lib/mutelist/mutelist.py b/prowler/lib/mutelist/mutelist.py
index f76d51a39f..8093489d78 100644
--- a/prowler/lib/mutelist/mutelist.py
+++ b/prowler/lib/mutelist/mutelist.py
@@ -153,8 +153,10 @@ class Mutelist(ABC):
Check if the provided finding is muted for the audited account, check, region, resource and tags.
The Mutelist works in a way that each field is ANDed, so if a check is muted for an account, region, resource and tags, it will be muted.
- The exceptions are ORed, so if a check is excepted for an account, region, resource or tags, it will not be muted.
- The only particularity is the tags, which are ORed.
+
+ Exceptions use AND logic across specified fields, with unspecified fields treated as wildcards (matching all values).
+
+ Tag matching uses AND logic when multiple tags are listed (all must match). OR logic is achieved using regex alternation (|) within a single tag pattern.
So, for the following Mutelist:
```
@@ -167,11 +169,16 @@ class Mutelist(ABC):
Resources:
- 'i-123456789'
Tags:
- - 'Name=AdminInstance | Environment=Prod'
+ - 'Name=AdminInstance|Environment=Prod'
Description: 'Field to describe why the findings associated with these values are muted'
```
The check `ec2_instance_detailed_monitoring_enabled` will be muted for all accounts and regions and for the resource_id 'i-123456789' with at least one of the tags 'Name=AdminInstance' or 'Environment=Prod'.
+ Note: The pipe (|) in the tag pattern provides OR logic via regex alternation. To require BOTH tags, use two separate tag entries:
+ Tags:
+ - 'Name=AdminInstance'
+ - 'Environment=Prod'
+
Args:
mutelist (dict): Dictionary containing information about muted checks for different accounts.
audited_account (str): The account being audited.
@@ -408,12 +415,13 @@ class Mutelist(ABC):
Args:
matched_items (list): List of items to be matched.
finding_items (str): String to search for matched items.
- tag (bool): If True the search will have a different logic due to the tags being ANDed or ORed:
- - Check of AND logic -> True if all the tags are present in the finding.
- - Check of OR logic -> True if any of the tags is present in the finding.
+ tag (bool): If True, uses AND logic across multiple tags in the list.
+ - Multiple tags: ALL tags in matched_items must be present in finding_items (AND logic).
+ - Single tag with regex alternation (|): Matches if pattern is found (enables OR within pattern).
+ - For non-tags: Uses OR logic - returns True if ANY item matches.
Returns:
- bool: True if any of the matched_items are present in finding_items, otherwise False.
+ bool: For tags - True if ALL patterns match. For non-tags - True if ANY pattern matches.
"""
try:
is_item_matched = False
From 8e07ec87276dc12eebd96ed2bb4b987919c3e50b Mon Sep 17 00:00:00 2001
From: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
Date: Tue, 11 Nov 2025 09:44:41 +0100
Subject: [PATCH 07/23] docs: refactor contributing docs (#9202)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
---
docs/developer-guide/documentation.mdx | 25 ++++-
docs/developer-guide/introduction.mdx | 133 ++++++++++++++++---------
2 files changed, 106 insertions(+), 52 deletions(-)
diff --git a/docs/developer-guide/documentation.mdx b/docs/developer-guide/documentation.mdx
index a7a4e97d4e..f1fae30d35 100644
--- a/docs/developer-guide/documentation.mdx
+++ b/docs/developer-guide/documentation.mdx
@@ -4,7 +4,26 @@ title: 'Contributing to Documentation'
Prowler documentation is built using [Mintlify](https://www.mintlify.com/docs), allowing contributors to easily add or enhance documentation.
-## Installation and Setup
+## Documentation Structure
+
+The Prowler documentation is organized into several sections. The main ones are:
+
+- **Getting Started**: Provides an overview of the Prowler platform and its different solutions, including Prowler Cloud/App, Prowler CLI, Prowler MCP Server, Prowler Hub, and Prowler Lighthouse AI. This section helps new users understand which Prowler solution best fits their needs and includes product comparisons.
+
+- **Guides**: Contains practical tutorials and how-to guides organized by product (Prowler Cloud/App, CLI) and provider (AWS, Azure, GCP, Kubernetes, Microsoft 365, GitHub, etc.). This section covers authentication, integrations, compliance, and advanced usage scenarios.
+
+- **Developer Guide**: Documentation for contributors looking to extend Prowler functionality. This includes guides on creating providers, services, checks, output formats, integrations, and compliance frameworks. Provider-specific implementation details and testing strategies are also covered here.
+
+- **Troubleshooting**: Common issues, error messages, and their solutions. This section helps users resolve problems encountered during installation, configuration, or execution.
+
+
+## AI-Driven Documentation
+
+As mentioned in the [Introduction](/developer-guide/introduction#ai-driven-contributions), we have specialized resources to enhance AI-driven development.
+
+This includes the [AGENTS.md](https://github.com/prowler-cloud/prowler/blob/master/docs/AGENTS.md) file that contains the guidelines and style guide for the AI agents in the Prowler documentation.
+
+## Local Development
@@ -33,10 +52,10 @@ Prowler documentation is built using [Mintlify](https://www.mintlify.com/docs),
- Once documentation updates are complete, submit a pull request for review.
+ Once documentation updates are complete, [submit a pull request for review](/developer-guide/introduction#sending-the-pull-request).
The Prowler team will assess and merge contributions.
-Your efforts help improve Prowler documentation—thank you for contributing!
+Your efforts help improve Prowler documentation. Thank you for contributing! 🤘
diff --git a/docs/developer-guide/introduction.mdx b/docs/developer-guide/introduction.mdx
index 5361d4460a..9d1ea742b1 100644
--- a/docs/developer-guide/introduction.mdx
+++ b/docs/developer-guide/introduction.mdx
@@ -2,19 +2,70 @@
title: 'Introduction to developing in Prowler'
---
-Extending Prowler
+Thanks for your interest in contributing to Prowler!
-Prowler can be extended in various ways, with common use cases including:
+Prowler can be extended in various ways. This guide provides the different ways to contribute and how to get started.
-- New security checks
-- New compliance frameworks
-- New output formats
-- New integrations
-- New proposed features
+## Contributing to Prowler
-All the relevant information for these cases is included in this guide.
+### Review Current Issues
+Check out our [GitHub Issues](https://github.com/prowler-cloud/prowler/issues) page for ideas to contribute.
+
+
+ We tag issues as `good first issue` for new contributors. These are typically well-defined and manageable in scope.
+
+
+ We tag issues as `help wanted` for other issues that require more time to complete.
+
+
-## Getting the Code and Installing All Dependencies
+### Expand Prowler's Capabilities
+Prowler is constantly evolving. Contributions to checks, services, or integrations help improve the tool for everyone. Here is how to get involved:
+
+
+
+ Want to improve Prowler's detection capabilities for your favorite cloud provider? You can contribute by writing new checks.
+
+
+ One key service for your favorite cloud provider is missing? Add it to Prowler! Do not forget to include relevant checks to validate functionality.
+
+
+ If you would like to extend Prowler to work with a new cloud provider, this typically involves setting up new services and checks to ensure compatibility.
+
+
+ Want to tailor how results are displayed or exported? You can add custom output formats.
+
+
+ Prowler can work with other tools and platforms through integrations.
+
+
+ Propose brand-new features or enhancements to existing ones, or help implement community-requested improvements.
+
+
+
+### Improve Documentation
+Help make Prowler more accessible by enhancing our documentation, fixing typos, or adding examples/tutorials.
+
+
+
+ Enhance our documentation, fix typos, or add examples/tutorials.
+
+
+
+### Bug Fixes
+If you find any issues or bugs, you can report them in the [GitHub Issues](https://github.com/prowler-cloud/prowler/issues) page and if you want you can also fix them.
+
+
+
+ Report or fix issues or bugs.
+
+
+
+Remember, our community is here to help! If you need guidance, do not hesitate to ask questions in the issues or join our [ Slack workspace](https://goto.prowler.com/slack).
+
+
+
+## Setting up your development environment
### Prerequisites
@@ -26,11 +77,11 @@ Before proceeding, ensure the following:
### Forking the Prowler Repository
-To contribute to Prowler, fork the Prowler GitHub repository. This allows you to propose changes, submit new features, and fix bugs. For guidance on forking, refer to the [official GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo?tool=webui#forking-a-repository).
+Fork the Prowler GitHub repository to contribute to Prowler. This allows proposing changes, submitting new features, and fixing bugs. For guidance on forking, refer to the [official GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo?tool=webui#forking-a-repository).
### Cloning Your Forked Repository
-Once your fork is created, clone it using the following commands:
+Once your fork is created, clone it using the following commands (replace `` with your GitHub username):
```
git clone https://github.com//prowler
@@ -56,39 +107,7 @@ If your poetry version is below 2.0.0 you must keep using `poetry shell` to acti
In case you have any doubts, consult the [Poetry environment activation guide](https://python-poetry.org/docs/managing-environments/#activating-the-environment).
-## Contributing to Prowler
-### Ways to Contribute
-
-Here are some ideas for collaborating with Prowler:
-
-1. **Review Current Issues**: Check out our [GitHub Issues](https://github.com/prowler-cloud/prowler/issues) page. We often tag issues as `good first issue` - these are perfect for new contributors as they are typically well-defined and manageable in scope.
-
-2. **Expand Prowler's Capabilities**: Prowler is constantly evolving, and you can be a part of its growth. Whether you are adding checks, supporting new services, or introducing integrations, your contributions help improve the tool for everyone. Here is how you can get involved:
-
- - **Adding New Checks**
- Want to improve Prowler's detection capabilities for your favorite cloud provider? You can contribute by writing new checks. To get started, follow the [create a new check guide](/developer-guide/checks).
-
- - **Adding New Services**
- One key service for your favorite cloud provider is missing? Add it to Prowler! To add a new service, check out the [create a new service guide](/developer-guide/services). Do not forget to include relevant checks to validate functionality.
-
- - **Adding New Providers**
- If you would like to extend Prowler to work with a new cloud provider, follow the [create a new provider guide](/developer-guide/provider). This typically involves setting up new services and checks to ensure compatibility.
-
- - **Adding New Output Formats**
- Want to tailor how results are displayed or exported? You can add custom output formats by following the [create a new output format guide](/developer-guide/outputs).
-
- - **Adding New Integrations**
- Prowler can work with other tools and platforms through integrations. If you would like to add one, see the [create a new integration guide](/developer-guide/integrations).
-
- - **Proposing or Implementing Features**
- Got an idea to make Prowler better? Whether it is a brand-new feature or an enhancement to an existing one, you are welcome to propose it or help implement community-requested improvements.
-
-3. **Improve Documentation**: Help make Prowler more accessible by enhancing our documentation, fixing typos, or adding examples/tutorials. See the tutorial of how we write our documentation [here](/developer-guide/documentation).
-
-4. **Bug Fixes**: If you find any issues or bugs, you can report them in the [GitHub Issues](https://github.com/prowler-cloud/prowler/issues) page and if you want you can also fix them.
-
-Remember, our community is here to help! If you need guidance, do not hesitate to ask questions in the issues or join our [Slack workspace](https://goto.prowler.com/slack).
### Pre-Commit Hooks
@@ -121,6 +140,16 @@ These should have been already installed if `poetry install --with dev` was alre
Additionally, ensure the latest version of [`TruffleHog`](https://github.com/trufflesecurity/trufflehog) is installed to scan for sensitive data in the code. Follow the official [installation guide](https://github.com/trufflesecurity/trufflehog?tab=readme-ov-file#floppy_disk-installation) for setup.
+### AI-Driven Contributions
+
+If you are using AI assistants to help with your contributions, Prowler provides specialized resources to enhance AI-driven development:
+
+- **Prowler MCP Server**: The [Prowler MCP Server](/getting-started/products/prowler-mcp) provides AI assistants with access to the entire Prowler ecosystem, including security checks, compliance frameworks, documentation, and more. This enables AI tools to better understand Prowler's architecture and help you create contributions that align with project standards.
+
+- **AGENTS.md Files**: Each component of the Prowler monorepo includes an `AGENTS.md` file that contains specific guidelines for AI agents working on that component. These files provide context about project structure, coding standards, and best practices. When working on a specific component, refer to the relevant `AGENTS.md` file (e.g., `prowler/AGENTS.md`, `ui/AGENTS.md`, `api/AGENTS.md`) to ensure your AI assistant follows the appropriate guidelines.
+
+These resources help ensure that AI-assisted contributions maintain consistency with Prowler's codebase and development practices.
+
### Dependency Management
All dependencies are listed in the `pyproject.toml` file.
@@ -133,7 +162,7 @@ If you encounter issues when committing to the Prowler repository, use the `--no
### Repository Folder Structure
-Understanding the layout of the Prowler codebase will help you quickly find where to add new features, checks, or integrations. The following is a high-level overview from the root of the repository:
+The Prowler codebase layout helps quickly locate where to add new features, checks, or integrations. The following is a high-level overview from the root of the repository:
```
prowler/
@@ -148,7 +177,7 @@ prowler/
├── permissions/ # Permission-related files and policies
├── contrib/ # Community-contributed scripts or modules
├── kubernetes/ # Kubernetes deployment files
-├── .github/ # GitHub related files (workflows, issue templates, etc.)
+├── .github/ # GitHub-related files (workflows, issue templates, etc.)
├── pyproject.toml # Python project configuration (Poetry)
├── poetry.lock # Poetry lock file
├── README.md # Project overview and getting started
@@ -158,19 +187,23 @@ prowler/
└── ... # Other supporting files
```
-## Pull Request Checklist
+## Sending the Pull Request
-When creating or reviewing a pull request in https://github.com/prowler-cloud/prowler, follow [this checklist](https://github.com/prowler-cloud/prowler/blob/master/.github/pull_request_template.md#checklist).
+When creating or reviewing a pull request in [Prowler](https://github.com/prowler-cloud/prowler), follow [this template](https://github.com/prowler-cloud/prowler/blob/master/.github/pull_request_template.md) and fill it with the relevant information:
+
+- **Context** and **Description** of the change: This will help the reviewers to understand the change and the purpose of the pull request.
+- **Steps to review**: A detailed description of how to review the change.
+- **Checklist**: A mandatory checklist of the things that should be reviewed before merging the pull request.
## Contribution Appreciation
-If you enjoy swag, we’d love to thank you for your contribution with laptop stickers or other Prowler merchandise!
+If you enjoy swag, we'd love to thank you for your contribution with laptop stickers or other Prowler merchandise!
To request swag: Share your pull request details in our [Slack workspace](https://goto.prowler.com/slack).
You can also reach out to Toni de la Fuente on [Twitter](https://twitter.com/ToniBlyx)—his DMs are open!
-# Testing a Pull Request from a Specific Branch
+## Testing a Pull Request from a Specific Branch
To test Prowler from a specific branch (for example, to try out changes from a pull request before it is merged), you can use `pipx` to install directly from GitHub:
@@ -179,3 +212,5 @@ pipx install "git+https://github.com/prowler-cloud/prowler.git@branch-name"
```
Replace `branch-name` with the name of the branch you want to test. This will install Prowler in an isolated environment, allowing you to try out the changes safely.
+
+For more details on testing go to the [Testing section](/developer-guide/unit-testing) of this documentation.
\ No newline at end of file
From 822d2011594d2d5550cc6977d00031448654ca46 Mon Sep 17 00:00:00 2001
From: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
Date: Tue, 11 Nov 2025 10:03:12 +0100
Subject: [PATCH 08/23] fix(github): hardcode list of prowler-cloud
organization members (#9207)
---
.github/workflows/labeler-community.yml | 35 ++++++++++++++++++++++---
1 file changed, 31 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/labeler-community.yml b/.github/workflows/labeler-community.yml
index c6115b4d30..d34ef6dfa2 100644
--- a/.github/workflows/labeler-community.yml
+++ b/.github/workflows/labeler-community.yml
@@ -20,12 +20,39 @@ jobs:
- name: Check if author is org member
id: check_membership
env:
- GH_TOKEN: ${{ github.token }}
AUTHOR: ${{ github.event.pull_request.user.login }}
- ORG: ${{ github.repository_owner }}
run: |
- echo "Checking if $AUTHOR is a member of $ORG"
- if gh api --method GET "orgs/$ORG/members/$AUTHOR" >/dev/null 2>&1; then
+ # Hardcoded list of prowler-cloud organization members
+ # This list includes members who have set their organization membership as private
+ ORG_MEMBERS=(
+ "AdriiiPRodri"
+ "Alan-TheGentleman"
+ "alejandrobailo"
+ "amitsharm"
+ "andoniaf"
+ "cesararroba"
+ "Chan9390"
+ "danibarranqueroo"
+ "HugoPBrito"
+ "jfagoagas"
+ "josemazo"
+ "lydiavilchez"
+ "mmuller88"
+ "MrCloudSec"
+ "pedrooot"
+ "prowler-bot"
+ "puchy22"
+ "rakan-pro"
+ "RosaRivasProwler"
+ "StylusFrost"
+ "toniblyx"
+ "vicferpoy"
+ )
+
+ echo "Checking if $AUTHOR is a member of prowler-cloud organization"
+
+ # Check if author is in the org members list
+ if printf '%s\n' "${ORG_MEMBERS[@]}" | grep -q "^${AUTHOR}$"; then
echo "is_member=true" >> $GITHUB_OUTPUT
echo "$AUTHOR is an organization member"
else
From 73a277f27b61c549c6094e42dd6b3bfae129a49c Mon Sep 17 00:00:00 2001
From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
Date: Tue, 11 Nov 2025 10:16:57 +0100
Subject: [PATCH 09/23] chore(m365_powershell): remove unnecessary
`test_credentials` (#9204)
---
prowler/CHANGELOG.md | 1 +
.../m365/lib/powershell/m365_powershell.py | 85 +-----
prowler/providers/m365/m365_provider.py | 7 +-
.../lib/powershell/m365_powershell_test.py | 252 +-----------------
tests/providers/m365/m365_provider_test.py | 31 ---
5 files changed, 13 insertions(+), 363 deletions(-)
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index e3823d9fa5..5ab695d1f3 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -42,6 +42,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS CodeArtifact service metadata to new format [(#8850)](https://github.com/prowler-cloud/prowler/pull/8850)
- Rename OCI provider to oraclecloud with oci alias [(#9126)](https://github.com/prowler-cloud/prowler/pull/9126)
+- Remove unnecessary tests for M365_PowerShell module [(#9204)](https://github.com/prowler-cloud/prowler/pull/9204)
---
diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py
index 797fd667ce..48e7a5abbd 100644
--- a/prowler/providers/m365/lib/powershell/m365_powershell.py
+++ b/prowler/providers/m365/lib/powershell/m365_powershell.py
@@ -2,10 +2,7 @@ import os
from prowler.lib.logger import logger
from prowler.lib.powershell.powershell import PowerShellSession
-from prowler.providers.m365.exceptions.exceptions import (
- M365CertificateCreationError,
- M365GraphConnectionError,
-)
+from prowler.providers.m365.exceptions.exceptions import M365CertificateCreationError
from prowler.providers.m365.lib.jwt.jwt_decoder import decode_msal_token
from prowler.providers.m365.models import M365Credentials, M365IdentityInfo
@@ -138,79 +135,6 @@ class M365PowerShell(PowerShellSession):
result = self.execute(command, timeout=connect_timeout)
return result or "'execute_connect' command timeout reached"
- def test_credentials(self, credentials: M365Credentials) -> bool:
- """
- Test Microsoft 365 credentials by attempting to authenticate against Entra ID.
-
- Supports testing two authentication methods:
- 1. Application authentication (client_id/client_secret)
- 2. Certificate authentication (certificate_content in base64/client_id)
-
- Args:
- credentials (M365Credentials): The credentials object containing
- authentication information to test.
-
- Returns:
- bool: True if credentials are valid and authentication succeeds, False otherwise.
- """
- # Test Certificate Auth
- if credentials.certificate_content and credentials.client_id:
- try:
- logger.info("Testing Microsoft Graph Certificate connection...")
- self.test_graph_certificate_connection()
- logger.info("Microsoft Graph Certificate connection successful")
- teams_connection_successful = self.test_teams_certificate_connection()
- if not teams_connection_successful:
- self.test_exchange_certificate_connection()
- return True
- except Exception as e:
- logger.error(f"Microsoft Graph Cer connection failed: {e}")
- raise M365GraphConnectionError(
- file=os.path.basename(__file__),
- original_exception=e,
- message="Check your Microsoft Application Certificate and ensure the app has proper permissions",
- )
- else:
- try:
- logger.info("Testing Microsoft Graph Client Secret connection...")
- self.test_graph_connection()
- logger.info("Microsoft Graph Client Secret connection successful")
- return True
- except Exception as e:
- logger.error(f"Microsoft Graph Client Secret connection failed: {e}")
- raise M365GraphConnectionError(
- file=os.path.basename(__file__),
- original_exception=e,
- message="Check your Microsoft Application Client Secret and ensure the app has proper permissions",
- )
-
- def test_graph_connection(self) -> bool:
- """Test Microsoft Graph API connection and raise exception if it fails."""
- try:
- if self.execute("Write-Output $graphToken") == "":
- raise M365GraphConnectionError(
- file=os.path.basename(__file__),
- message="Microsoft Graph token is empty or invalid.",
- )
- return True
- except Exception as e:
- logger.error(f"Microsoft Graph connection failed: {e}")
- raise M365GraphConnectionError(
- file=os.path.basename(__file__),
- original_exception=e,
- message=f"Failed to connect to Microsoft Graph API: {str(e)}",
- )
-
- def test_graph_certificate_connection(self) -> bool:
- """Test Microsoft Graph API connection using certificate and raise exception if it fails."""
- result = self.execute_connect(
- "Connect-Graph -Certificate $certificate -AppId $clientID -TenantId $tenantID"
- )
- if "Welcome to Microsoft Graph!" not in result:
- logger.error(f"Microsoft Graph Certificate connection failed: {result}")
- return False
- return True
-
def test_teams_connection(self) -> bool:
"""Test Microsoft Teams API connection and raise exception if it fails."""
try:
@@ -926,7 +850,10 @@ def initialize_m365_powershell_modules():
bool: True if all modules were successfully initialized, False otherwise
"""
- REQUIRED_MODULES = ["ExchangeOnlineManagement", "MicrosoftTeams", "MSAL.PS"]
+ REQUIRED_MODULES = [
+ "ExchangeOnlineManagement",
+ "MicrosoftTeams",
+ ]
pwsh = PowerShellSession()
try:
@@ -938,7 +865,7 @@ def initialize_m365_powershell_modules():
# Install module if not installed
if not result:
install_result = pwsh.execute(
- f'Install-Module "{module}" -Force -AllowClobber -Scope CurrentUser',
+ f"Install-Module {module} -Force -AllowClobber -Scope CurrentUser",
timeout=60,
)
if install_result:
diff --git a/prowler/providers/m365/m365_provider.py b/prowler/providers/m365/m365_provider.py
index 801b02027f..f6ef545a7c 100644
--- a/prowler/providers/m365/m365_provider.py
+++ b/prowler/providers/m365/m365_provider.py
@@ -444,12 +444,7 @@ class M365Provider(Provider):
try:
if init_modules:
initialize_m365_powershell_modules()
- if test_session.test_credentials(credentials):
- return credentials
- raise M365ConfigCredentialsError(
- file=os.path.basename(__file__),
- message="The provided credentials are not valid.",
- )
+ return credentials
finally:
test_session.close()
diff --git a/tests/providers/m365/lib/powershell/m365_powershell_test.py b/tests/providers/m365/lib/powershell/m365_powershell_test.py
index 28b09768c3..b6ba890c2c 100644
--- a/tests/providers/m365/lib/powershell/m365_powershell_test.py
+++ b/tests/providers/m365/lib/powershell/m365_powershell_test.py
@@ -4,10 +4,7 @@ from unittest.mock import MagicMock, call, patch
import pytest
from prowler.lib.powershell.powershell import PowerShellSession
-from prowler.providers.m365.exceptions.exceptions import (
- M365CertificateCreationError,
- M365GraphConnectionError,
-)
+from prowler.providers.m365.exceptions.exceptions import M365CertificateCreationError
from prowler.providers.m365.lib.powershell.m365_powershell import M365PowerShell
from prowler.providers.m365.models import M365Credentials, M365IdentityInfo
@@ -115,31 +112,6 @@ class Testm365PowerShell:
)
session.close()
- @patch("subprocess.Popen")
- def test_test_credentials_application_auth(self, mock_popen):
- mock_process = MagicMock()
- mock_popen.return_value = mock_process
- credentials = M365Credentials(
- client_id="test_client_id",
- client_secret="test_client_secret",
- tenant_id="test_tenant_id",
- )
- identity = M365IdentityInfo(
- identity_id="test_id",
- identity_type="Service Principal",
- tenant_id="test_tenant",
- tenant_domain="contoso.onmicrosoft.com",
- tenant_domains=["contoso.onmicrosoft.com"],
- location="test_location",
- )
- session = M365PowerShell(credentials, identity)
- session.execute = MagicMock(return_value="sometoken")
-
- result = session.test_credentials(credentials)
- assert result is True
- session.execute.assert_any_call("Write-Output $graphToken")
- session.close()
-
@patch("subprocess.Popen")
def test_remove_ansi(self, mock_popen):
credentials = M365Credentials(
@@ -339,13 +311,14 @@ class Testm365PowerShell:
# Verify successful initialization
assert result is True
# Verify that execute was called for each module
- assert mock_execute_obj.call_count == 9 # 3 modules * 3 commands each
+ assert (
+ mock_execute_obj.call_count == 2 * 3
+ ) # number of modules * 3 commands each
# Verify success messages were logged
mock_info.assert_any_call(
"Successfully installed module ExchangeOnlineManagement"
)
mock_info.assert_any_call("Successfully installed module MicrosoftTeams")
- mock_info.assert_any_call("Successfully installed module MSAL.PS")
@patch("subprocess.Popen")
def test_initialize_m365_powershell_modules_failure(self, mock_popen):
@@ -408,12 +381,11 @@ class Testm365PowerShell:
main()
# Verify all info messages were logged in the correct order
- assert mock_info.call_count == 4
+ assert mock_info.call_count == 3
mock_info.assert_has_calls(
[
call("Successfully installed module ExchangeOnlineManagement"),
call("Successfully installed module MicrosoftTeams"),
- call("Successfully installed module MSAL.PS"),
call("M365 PowerShell modules initialized successfully"),
]
)
@@ -456,96 +428,6 @@ class Testm365PowerShell:
# Verify no info messages were logged
mock_info.assert_not_called()
- @patch("subprocess.Popen")
- def test_test_graph_connection_success(self, mock_popen):
- """Test test_graph_connection when token is valid"""
- mock_process = MagicMock()
- mock_popen.return_value = mock_process
- credentials = M365Credentials(
- client_id="test_client_id",
- client_secret="test_client_secret",
- tenant_id="test_tenant_id",
- )
- identity = M365IdentityInfo(
- identity_id="test_id",
- identity_type="Application",
- tenant_id="test_tenant",
- tenant_domain="example.com",
- tenant_domains=["example.com"],
- location="test_location",
- )
- session = M365PowerShell(credentials, identity)
-
- # Mock execute to return a valid token
- session.execute = MagicMock(return_value="valid_token")
-
- result = session.test_graph_connection()
-
- assert result is True
- session.execute.assert_called_once_with("Write-Output $graphToken")
- session.close()
-
- @patch("subprocess.Popen")
- def test_test_graph_connection_empty_token(self, mock_popen):
- """Test test_graph_connection when token is empty"""
- mock_process = MagicMock()
- mock_popen.return_value = mock_process
- credentials = M365Credentials(
- client_id="test_client_id",
- client_secret="test_client_secret",
- tenant_id="test_tenant_id",
- )
- identity = M365IdentityInfo(
- identity_id="test_id",
- identity_type="Application",
- tenant_id="test_tenant",
- tenant_domain="example.com",
- tenant_domains=["example.com"],
- location="test_location",
- )
- session = M365PowerShell(credentials, identity)
-
- # Mock execute to return empty token
- session.execute = MagicMock(return_value="")
-
- with pytest.raises(M365GraphConnectionError) as exc_info:
- session.test_graph_connection()
-
- assert "Microsoft Graph token is empty or invalid" in str(exc_info.value)
- session.execute.assert_called_once_with("Write-Output $graphToken")
- session.close()
-
- @patch("subprocess.Popen")
- def test_test_graph_connection_exception(self, mock_popen):
- """Test test_graph_connection when an exception occurs"""
- mock_process = MagicMock()
- mock_popen.return_value = mock_process
- credentials = M365Credentials(
- client_id="test_client_id",
- client_secret="test_client_secret",
- tenant_id="test_tenant_id",
- )
- identity = M365IdentityInfo(
- identity_id="test_id",
- identity_type="Application",
- tenant_id="test_tenant",
- tenant_domain="example.com",
- tenant_domains=["example.com"],
- location="test_location",
- )
- session = M365PowerShell(credentials, identity)
-
- # Mock execute to raise an exception
- session.execute = MagicMock(side_effect=Exception("PowerShell error"))
-
- with pytest.raises(M365GraphConnectionError) as exc_info:
- session.test_graph_connection()
-
- assert "Failed to connect to Microsoft Graph API: PowerShell error" in str(
- exc_info.value
- )
- session.close()
-
@patch("subprocess.Popen")
def test_test_teams_connection_success(self, mock_popen):
"""Test test_teams_connection when token is valid"""
@@ -1007,36 +889,6 @@ class Testm365PowerShell:
session.close()
- @patch("subprocess.Popen")
- def test_test_credentials_certificate_auth_success(self, mock_popen):
- """Test test_credentials method with certificate authentication - successful"""
- mock_process = MagicMock()
- mock_popen.return_value = mock_process
-
- certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8")
- credentials = M365Credentials(
- client_id="test_client_id", certificate_content=certificate_content
- )
- identity = M365IdentityInfo()
-
- # Create session without calling init_credential
- with patch.object(M365PowerShell, "init_credential"):
- session = M365PowerShell(credentials, identity)
-
- # Mock successful certificate connections
- # Note: The actual implementation uses "or" so if teams succeeds, exchange won't be called
- session.test_teams_certificate_connection = MagicMock(return_value=True)
- session.test_exchange_certificate_connection = MagicMock(return_value=True)
-
- result = session.test_credentials(credentials)
- assert result is True
-
- session.test_teams_certificate_connection.assert_called_once()
- # Exchange connection should NOT be called if teams connection succeeds (due to "or" logic)
- session.test_exchange_certificate_connection.assert_not_called()
-
- session.close()
-
@patch("subprocess.Popen")
def test_test_credentials_certificate_auth_failure(self, mock_popen):
"""Test test_credentials method with certificate authentication - failure"""
@@ -1057,9 +909,6 @@ class Testm365PowerShell:
session.test_teams_certificate_connection = MagicMock(return_value=False)
session.test_exchange_certificate_connection = MagicMock(return_value=False)
- result = session.test_credentials(credentials)
- assert result is True # Method always returns True after the try block
-
session.close()
@patch("subprocess.Popen")
@@ -1287,94 +1136,3 @@ class Testm365PowerShell:
assert any('$tenantDomain = "contoso.com"' in cmd for cmd in executed_commands)
session.close()
-
- @patch("subprocess.Popen")
- def test_test_credentials_certificate_auth_with_or_logic(self, mock_popen):
- """Test test_credentials method with certificate auth using OR logic between Teams and Exchange"""
- certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8")
-
- mock_process = MagicMock()
- mock_popen.return_value = mock_process
- mock_process.returncode = 0
-
- # Create session with non-certificate credentials first
- session = M365PowerShell(
- M365Credentials(
- client_id="test_client_id",
- client_secret="test_secret",
- tenant_id="test_tenant_id",
- tenant_domains=["contoso.com"],
- ),
- M365IdentityInfo(
- tenant_id="test_tenant_id",
- tenant_domain="contoso.com",
- tenant_domains=["contoso.com"],
- identity_id="test_identity_id",
- identity_type="Service Principal with Certificate",
- ),
- )
-
- # Mock that Teams connection fails but Exchange succeeds
- session.test_teams_certificate_connection = MagicMock(return_value=False)
- session.test_exchange_certificate_connection = MagicMock(return_value=True)
-
- result = session.test_credentials(
- M365Credentials(
- client_id="test_client_id",
- tenant_id="test_tenant_id",
- certificate_content=certificate_content,
- tenant_domains=["contoso.com"],
- )
- )
-
- assert result is True
- session.test_teams_certificate_connection.assert_called_once()
- session.test_exchange_certificate_connection.assert_called_once()
-
- session.close()
-
- @patch("subprocess.Popen")
- def test_test_credentials_certificate_auth_both_fail(self, mock_popen):
- """Test test_credentials method with certificate auth when both Teams and Exchange fail"""
- certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8")
-
- mock_process = MagicMock()
- mock_popen.return_value = mock_process
- mock_process.returncode = 0
-
- # Create session with non-certificate credentials first
- session = M365PowerShell(
- M365Credentials(
- client_id="test_client_id",
- client_secret="test_secret",
- tenant_id="test_tenant_id",
- tenant_domains=["contoso.com"],
- ),
- M365IdentityInfo(
- tenant_id="test_tenant_id",
- tenant_domain="contoso.com",
- tenant_domains=["contoso.com"],
- identity_id="test_identity_id",
- identity_type="Service Principal with Certificate",
- ),
- )
-
- # Mock that both connections fail
- session.test_teams_certificate_connection = MagicMock(return_value=False)
- session.test_exchange_certificate_connection = MagicMock(return_value=False)
-
- # Even when both fail, the method should return True (this is the intended logic)
- result = session.test_credentials(
- M365Credentials(
- client_id="test_client_id",
- tenant_id="test_tenant_id",
- certificate_content=certificate_content,
- tenant_domains=["contoso.com"],
- )
- )
-
- assert result is True
- session.test_teams_certificate_connection.assert_called_once()
- session.test_exchange_certificate_connection.assert_called_once()
-
- session.close()
diff --git a/tests/providers/m365/m365_provider_test.py b/tests/providers/m365/m365_provider_test.py
index 53223cc22a..f2354ea7e7 100644
--- a/tests/providers/m365/m365_provider_test.py
+++ b/tests/providers/m365/m365_provider_test.py
@@ -932,37 +932,6 @@ class TestM365Provider:
assert result.certificate_content == certificate_content
assert identity.identity_type == "Service Principal with Certificate"
- def test_setup_powershell_invalid_credentials(self):
- """Test setup_powershell with invalid credentials"""
- credentials_dict = {
- "client_id": "test_client_id",
- "tenant_id": "test_tenant_id",
- "client_secret": "test_client_secret",
- }
-
- with (
- patch("prowler.providers.m365.m365_provider.M365PowerShell") as mock_ps,
- pytest.raises(M365ConfigCredentialsError) as exception,
- ):
- mock_session = MagicMock()
- mock_session.test_credentials.return_value = False
- mock_session.close = MagicMock()
- mock_ps.return_value = mock_session
-
- M365Provider.setup_powershell(
- m365_credentials=credentials_dict,
- identity=M365IdentityInfo(
- identity_id=IDENTITY_ID,
- identity_type="User",
- tenant_id=TENANT_ID,
- tenant_domain=DOMAIN,
- tenant_domains=["test.onmicrosoft.com"],
- location=LOCATION,
- ),
- )
- assert exception.type == M365ConfigCredentialsError
- assert "The provided credentials are not valid." in str(exception.value)
-
def test_validate_arguments_browser_auth_without_tenant_id(self):
"""Test validate_arguments with browser_auth but missing tenant_id"""
with pytest.raises(M365BrowserAuthNoTenantIDError) as exception:
From beec37b0da3667fe30addccb95a9d92f527fb4df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adri=C3=A1n=20Jes=C3=BAs=20Pe=C3=B1a=20Rodr=C3=ADguez?=
Date: Tue, 11 Nov 2025 10:19:48 +0100
Subject: [PATCH 10/23] feat(threatscore): implement ThreatScoreSnapshot model,
filter, serializer, and view for ThreatScore metrics retrieval (#9148)
---
api/CHANGELOG.md | 1 +
api/src/backend/api/filters.py | 34 ++
.../migrations/0057_threatscoresnapshot.py | 170 ++++++++
api/src/backend/api/models.py | 134 ++++++
api/src/backend/api/tests/test_views.py | 403 ++++++++++++++++++
api/src/backend/api/v1/serializers.py | 62 +++
api/src/backend/api/v1/views.py | 352 ++++++++++++++-
api/src/backend/tasks/jobs/report.py | 207 ++++-----
api/src/backend/tasks/jobs/threatscore.py | 214 ++++++++++
.../backend/tasks/jobs/threatscore_utils.py | 127 ++++++
api/src/backend/tasks/tests/test_report.py | 157 ++++++-
11 files changed, 1721 insertions(+), 140 deletions(-)
create mode 100644 api/src/backend/api/migrations/0057_threatscoresnapshot.py
create mode 100644 api/src/backend/tasks/jobs/threatscore.py
create mode 100644 api/src/backend/tasks/jobs/threatscore_utils.py
diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md
index b9b0583c68..5df4b97764 100644
--- a/api/CHANGELOG.md
+++ b/api/CHANGELOG.md
@@ -14,6 +14,7 @@ All notable changes to the **Prowler API** are documented in this file.
- Support muting findings based on simple rules with custom reason [(#9051)](https://github.com/prowler-cloud/prowler/pull/9051)
- Support C5 compliance framework for the GCP provider [(#9097)](https://github.com/prowler-cloud/prowler/pull/9097)
- Support for Amazon Bedrock and OpenAI compatible providers in Lighthouse AI [(#8957)](https://github.com/prowler-cloud/prowler/pull/8957)
+- Tenant-wide ThreatScore overview aggregation and snapshot persistence with backfill support [(#9148)](https://github.com/prowler-cloud/prowler/pull/9148)
- Support for MongoDB Atlas provider [(#9167)](https://github.com/prowler-cloud/prowler/pull/9167)
---
diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py
index a3cb389351..1d863267f2 100644
--- a/api/src/backend/api/filters.py
+++ b/api/src/backend/api/filters.py
@@ -47,6 +47,7 @@ from api.models import (
StatusChoices,
Task,
TenantAPIKey,
+ ThreatScoreSnapshot,
User,
)
from api.rls import Tenant
@@ -998,3 +999,36 @@ class MuteRuleFilter(FilterSet):
"inserted_at": ["gte", "lte"],
"updated_at": ["gte", "lte"],
}
+
+
+class ThreatScoreSnapshotFilter(FilterSet):
+ """
+ Filter for ThreatScore snapshots.
+ Allows filtering by scan, provider, compliance_id, and date ranges.
+ """
+
+ inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
+ scan_id = UUIDFilter(field_name="scan__id", lookup_expr="exact")
+ scan_id__in = UUIDInFilter(field_name="scan__id", lookup_expr="in")
+ provider_id = UUIDFilter(field_name="provider__id", lookup_expr="exact")
+ provider_id__in = UUIDInFilter(field_name="provider__id", lookup_expr="in")
+ provider_type = ChoiceFilter(
+ field_name="provider__provider", choices=Provider.ProviderChoices.choices
+ )
+ provider_type__in = ChoiceInFilter(
+ field_name="provider__provider",
+ choices=Provider.ProviderChoices.choices,
+ lookup_expr="in",
+ )
+ compliance_id = CharFilter(field_name="compliance_id", lookup_expr="exact")
+ compliance_id__in = CharInFilter(field_name="compliance_id", lookup_expr="in")
+
+ class Meta:
+ model = ThreatScoreSnapshot
+ fields = {
+ "scan": ["exact", "in"],
+ "provider": ["exact", "in"],
+ "compliance_id": ["exact", "in"],
+ "inserted_at": ["date", "gte", "lte"],
+ "overall_score": ["exact", "gte", "lte"],
+ }
diff --git a/api/src/backend/api/migrations/0057_threatscoresnapshot.py b/api/src/backend/api/migrations/0057_threatscoresnapshot.py
new file mode 100644
index 0000000000..ee3530a5b6
--- /dev/null
+++ b/api/src/backend/api/migrations/0057_threatscoresnapshot.py
@@ -0,0 +1,170 @@
+# Generated by Django 5.1.13 on 2025-10-31 09:04
+
+import uuid
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+import api.rls
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("api", "0056_remove_provider_unique_provider_uids_and_more"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="ThreatScoreSnapshot",
+ fields=[
+ (
+ "id",
+ models.UUIDField(
+ default=uuid.uuid4,
+ editable=False,
+ primary_key=True,
+ serialize=False,
+ ),
+ ),
+ ("inserted_at", models.DateTimeField(auto_now_add=True)),
+ (
+ "compliance_id",
+ models.CharField(
+ help_text="Compliance framework ID (e.g., 'prowler_threatscore_aws')",
+ max_length=100,
+ ),
+ ),
+ (
+ "overall_score",
+ models.DecimalField(
+ decimal_places=2,
+ help_text="Overall ThreatScore percentage (0-100)",
+ max_digits=5,
+ ),
+ ),
+ (
+ "score_delta",
+ models.DecimalField(
+ blank=True,
+ decimal_places=2,
+ help_text="Score change compared to previous snapshot (positive = improvement)",
+ max_digits=5,
+ null=True,
+ ),
+ ),
+ (
+ "section_scores",
+ models.JSONField(
+ blank=True,
+ default=dict,
+ help_text="ThreatScore breakdown by section",
+ ),
+ ),
+ (
+ "critical_requirements",
+ models.JSONField(
+ blank=True,
+ default=list,
+ help_text="List of critical failed requirements (risk >= 4)",
+ ),
+ ),
+ (
+ "total_requirements",
+ models.IntegerField(
+ default=0, help_text="Total number of requirements evaluated"
+ ),
+ ),
+ (
+ "passed_requirements",
+ models.IntegerField(
+ default=0, help_text="Number of requirements with PASS status"
+ ),
+ ),
+ (
+ "failed_requirements",
+ models.IntegerField(
+ default=0, help_text="Number of requirements with FAIL status"
+ ),
+ ),
+ (
+ "manual_requirements",
+ models.IntegerField(
+ default=0, help_text="Number of requirements with MANUAL status"
+ ),
+ ),
+ (
+ "total_findings",
+ models.IntegerField(
+ default=0,
+ help_text="Total number of findings across all requirements",
+ ),
+ ),
+ (
+ "passed_findings",
+ models.IntegerField(
+ default=0, help_text="Number of findings with PASS status"
+ ),
+ ),
+ (
+ "failed_findings",
+ models.IntegerField(
+ default=0, help_text="Number of findings with FAIL status"
+ ),
+ ),
+ (
+ "provider",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="threatscore_snapshots",
+ related_query_name="threatscore_snapshot",
+ to="api.provider",
+ ),
+ ),
+ (
+ "scan",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="threatscore_snapshots",
+ related_query_name="threatscore_snapshot",
+ to="api.scan",
+ ),
+ ),
+ (
+ "tenant",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
+ ),
+ ),
+ ],
+ options={
+ "db_table": "threatscore_snapshots",
+ "abstract": False,
+ },
+ ),
+ migrations.AddIndex(
+ model_name="threatscoresnapshot",
+ index=models.Index(
+ fields=["tenant_id", "scan_id"], name="threatscore_snap_t_scan_idx"
+ ),
+ ),
+ migrations.AddIndex(
+ model_name="threatscoresnapshot",
+ index=models.Index(
+ fields=["tenant_id", "provider_id"], name="threatscore_snap_t_prov_idx"
+ ),
+ ),
+ migrations.AddIndex(
+ model_name="threatscoresnapshot",
+ index=models.Index(
+ fields=["tenant_id", "inserted_at"], name="threatscore_snap_t_time_idx"
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="threatscoresnapshot",
+ constraint=api.rls.RowLevelSecurityConstraint(
+ "tenant_id",
+ name="rls_on_threatscoresnapshot",
+ statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
+ ),
+ ),
+ ]
diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py
index 49dbc5f35c..536c5a65f6 100644
--- a/api/src/backend/api/models.py
+++ b/api/src/backend/api/models.py
@@ -2239,3 +2239,137 @@ class LighthouseProviderModels(RowLevelSecurityProtectedModel):
class JSONAPIMeta:
resource_name = "lighthouse-models"
+
+
+class ThreatScoreSnapshot(RowLevelSecurityProtectedModel):
+ """
+ Stores historical ThreatScore metrics for a given scan.
+ Snapshots are created automatically after each ThreatScore report generation.
+ """
+
+ objects = models.Manager()
+ all_objects = models.Manager()
+
+ id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
+ inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
+
+ scan = models.ForeignKey(
+ Scan,
+ on_delete=models.CASCADE,
+ related_name="threatscore_snapshots",
+ related_query_name="threatscore_snapshot",
+ )
+
+ provider = models.ForeignKey(
+ Provider,
+ on_delete=models.CASCADE,
+ related_name="threatscore_snapshots",
+ related_query_name="threatscore_snapshot",
+ )
+
+ compliance_id = models.CharField(
+ max_length=100,
+ blank=False,
+ null=False,
+ help_text="Compliance framework ID (e.g., 'prowler_threatscore_aws')",
+ )
+
+ # Overall ThreatScore metrics
+ overall_score = models.DecimalField(
+ max_digits=5,
+ decimal_places=2,
+ help_text="Overall ThreatScore percentage (0-100)",
+ )
+
+ # Score improvement/degradation compared to previous snapshot
+ score_delta = models.DecimalField(
+ max_digits=5,
+ decimal_places=2,
+ null=True,
+ blank=True,
+ help_text="Score change compared to previous snapshot (positive = improvement)",
+ )
+
+ # Section breakdown stored as JSON
+ # Format: {"1. IAM": 85.5, "2. Attack Surface": 92.3, ...}
+ section_scores = models.JSONField(
+ default=dict,
+ blank=True,
+ help_text="ThreatScore breakdown by section",
+ )
+
+ # Critical requirements metadata stored as JSON
+ # Format: [{"requirement_id": "...", "risk_level": 5, "weight": 150, ...}, ...]
+ critical_requirements = models.JSONField(
+ default=list,
+ blank=True,
+ help_text="List of critical failed requirements (risk >= 4)",
+ )
+
+ # Summary statistics
+ total_requirements = models.IntegerField(
+ default=0,
+ help_text="Total number of requirements evaluated",
+ )
+
+ passed_requirements = models.IntegerField(
+ default=0,
+ help_text="Number of requirements with PASS status",
+ )
+
+ failed_requirements = models.IntegerField(
+ default=0,
+ help_text="Number of requirements with FAIL status",
+ )
+
+ manual_requirements = models.IntegerField(
+ default=0,
+ help_text="Number of requirements with MANUAL status",
+ )
+
+ total_findings = models.IntegerField(
+ default=0,
+ help_text="Total number of findings across all requirements",
+ )
+
+ passed_findings = models.IntegerField(
+ default=0,
+ help_text="Number of findings with PASS status",
+ )
+
+ failed_findings = models.IntegerField(
+ default=0,
+ help_text="Number of findings with FAIL status",
+ )
+
+ def __str__(self):
+ return f"ThreatScore {self.overall_score}% for scan {self.scan_id} ({self.inserted_at})"
+
+ class Meta(RowLevelSecurityProtectedModel.Meta):
+ db_table = "threatscore_snapshots"
+
+ constraints = [
+ RowLevelSecurityConstraint(
+ field="tenant_id",
+ name="rls_on_%(class)s",
+ statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
+ ),
+ ]
+
+ indexes = [
+ models.Index(
+ fields=["tenant_id", "scan_id"],
+ name="threatscore_snap_t_scan_idx",
+ ),
+ models.Index(
+ fields=["tenant_id", "provider_id"],
+ name="threatscore_snap_t_prov_idx",
+ ),
+ models.Index(
+ fields=["tenant_id", "inserted_at"],
+ name="threatscore_snap_t_time_idx",
+ ),
+ ]
+
+ class JSONAPIMeta:
+ resource_name = "threatscore-snapshots"
diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py
index 7da8a095a1..fc433e1e95 100644
--- a/api/src/backend/api/tests/test_views.py
+++ b/api/src/backend/api/tests/test_views.py
@@ -4,6 +4,7 @@ import json
import os
import tempfile
from datetime import datetime, timedelta, timezone
+from decimal import Decimal
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import ANY, MagicMock, Mock, patch
@@ -56,6 +57,7 @@ from api.models import (
StateChoices,
Task,
TenantAPIKey,
+ ThreatScoreSnapshot,
User,
UserRoleRelationship,
)
@@ -6221,6 +6223,407 @@ class TestOverviewViewSet:
for entry in grouped_data:
assert "findings" not in entry["attributes"]
+ def _create_scan(self, tenant, provider, name, started_at=None):
+ scan_started = started_at or datetime.now(timezone.utc) - timedelta(hours=1)
+ return Scan.objects.create(
+ tenant=tenant,
+ provider=provider,
+ name=name,
+ trigger=Scan.TriggerChoices.MANUAL,
+ state=StateChoices.COMPLETED,
+ started_at=scan_started,
+ completed_at=scan_started + timedelta(minutes=30),
+ )
+
+ def _create_threatscore_snapshot(
+ self,
+ tenant,
+ scan,
+ provider,
+ *,
+ compliance_id,
+ overall_score,
+ score_delta,
+ section_scores,
+ critical_requirements,
+ total_requirements,
+ passed_requirements,
+ failed_requirements,
+ manual_requirements,
+ total_findings,
+ passed_findings,
+ failed_findings,
+ ):
+ return ThreatScoreSnapshot.objects.create(
+ tenant=tenant,
+ scan=scan,
+ provider=provider,
+ compliance_id=compliance_id,
+ overall_score=Decimal(overall_score),
+ score_delta=Decimal(score_delta) if score_delta is not None else None,
+ section_scores=section_scores,
+ critical_requirements=critical_requirements,
+ total_requirements=total_requirements,
+ passed_requirements=passed_requirements,
+ failed_requirements=failed_requirements,
+ manual_requirements=manual_requirements,
+ total_findings=total_findings,
+ passed_findings=passed_findings,
+ failed_findings=failed_findings,
+ )
+
+ def test_overview_threatscore_returns_weighted_aggregate_snapshot(
+ self, authenticated_client, tenants_fixture, providers_fixture
+ ):
+ tenant = tenants_fixture[0]
+ provider1, provider2, *_ = providers_fixture
+
+ scan1 = self._create_scan(tenant, provider1, "agg-scan-one")
+ scan2 = self._create_scan(tenant, provider2, "agg-scan-two")
+
+ snapshot1 = self._create_threatscore_snapshot(
+ tenant,
+ scan1,
+ provider1,
+ compliance_id="prowler_threatscore_aws",
+ overall_score="80.00",
+ score_delta="5.00",
+ section_scores={"1. IAM": "70.00", "2. Attack Surface": "60.00"},
+ critical_requirements=[
+ {
+ "requirement_id": "req_shared",
+ "title": "Shared requirement (preferred)",
+ "section": "1. IAM",
+ "subsection": "Sub IAM",
+ "risk_level": 5,
+ "weight": 150,
+ "passed_findings": 14,
+ "total_findings": 20,
+ "description": "Higher risk duplicate",
+ },
+ {
+ "requirement_id": "req_unique_one",
+ "title": "Unique provider one",
+ "section": "2. Attack Surface",
+ "subsection": "Sub Attack",
+ "risk_level": 4,
+ "weight": 90,
+ "passed_findings": 20,
+ "total_findings": 30,
+ "description": "Lower risk",
+ },
+ ],
+ total_requirements=120,
+ passed_requirements=90,
+ failed_requirements=30,
+ manual_requirements=0,
+ total_findings=100,
+ passed_findings=70,
+ failed_findings=30,
+ )
+
+ snapshot2 = self._create_threatscore_snapshot(
+ tenant,
+ scan2,
+ provider2,
+ compliance_id="prowler_threatscore_aws",
+ overall_score="20.00",
+ score_delta="-2.00",
+ section_scores={
+ "1. IAM": "10.00",
+ "2. Attack Surface": "40.00",
+ "3. Logging": "30.00",
+ },
+ critical_requirements=[
+ {
+ "requirement_id": "req_shared",
+ "title": "Shared requirement (secondary)",
+ "section": "1. IAM",
+ "subsection": "Sub IAM",
+ "risk_level": 4,
+ "weight": 120,
+ "passed_findings": 8,
+ "total_findings": 12,
+ "description": "Lower risk duplicate",
+ },
+ {
+ "requirement_id": "req_unique_two",
+ "title": "Unique provider two",
+ "section": "3. Logging",
+ "subsection": "Sub Logging",
+ "risk_level": 5,
+ "weight": 110,
+ "passed_findings": 6,
+ "total_findings": 10,
+ "description": "Another critical requirement",
+ },
+ ],
+ total_requirements=80,
+ passed_requirements=30,
+ failed_requirements=50,
+ manual_requirements=0,
+ total_findings=50,
+ passed_findings=15,
+ failed_findings=35,
+ )
+
+ older_inserted = datetime(2025, 1, 1, 12, 0, tzinfo=timezone.utc)
+ newer_inserted = datetime(2025, 1, 2, 12, 0, tzinfo=timezone.utc)
+ ThreatScoreSnapshot.objects.filter(id=snapshot1.id).update(
+ inserted_at=older_inserted
+ )
+ ThreatScoreSnapshot.objects.filter(id=snapshot2.id).update(
+ inserted_at=newer_inserted
+ )
+ snapshot2.refresh_from_db()
+
+ response = authenticated_client.get(reverse("overview-threatscore"))
+
+ assert response.status_code == status.HTTP_200_OK
+ body = response.json()
+ assert len(body["data"]) == 1
+ aggregated = body["data"][0]
+
+ assert aggregated["id"] == "n/a"
+ assert aggregated["relationships"]["scan"]["data"] is None
+ assert aggregated["relationships"]["provider"]["data"] is None
+
+ attrs = aggregated["attributes"]
+ assert Decimal(attrs["overall_score"]) == Decimal("60.00")
+ assert Decimal(attrs["score_delta"]) == Decimal("2.67")
+ assert attrs["inserted_at"] == snapshot2.inserted_at.isoformat().replace(
+ "+00:00", "Z"
+ )
+ assert attrs["total_findings"] == 150
+ assert attrs["passed_findings"] == 85
+ assert attrs["failed_findings"] == 65
+ assert attrs["total_requirements"] == 200
+ assert attrs["passed_requirements"] == 120
+ assert attrs["failed_requirements"] == 80
+ assert attrs["manual_requirements"] == 0
+
+ assert attrs["section_scores"] == {
+ "1. IAM": "50.00",
+ "2. Attack Surface": "53.33",
+ "3. Logging": "30.00",
+ }
+
+ expected_critical = [
+ {
+ "requirement_id": "req_shared",
+ "title": "Shared requirement (preferred)",
+ "section": "1. IAM",
+ "subsection": "Sub IAM",
+ "risk_level": 5,
+ "weight": 150,
+ "passed_findings": 14,
+ "total_findings": 20,
+ "description": "Higher risk duplicate",
+ },
+ {
+ "requirement_id": "req_unique_two",
+ "title": "Unique provider two",
+ "section": "3. Logging",
+ "subsection": "Sub Logging",
+ "risk_level": 5,
+ "weight": 110,
+ "passed_findings": 6,
+ "total_findings": 10,
+ "description": "Another critical requirement",
+ },
+ {
+ "requirement_id": "req_unique_one",
+ "title": "Unique provider one",
+ "section": "2. Attack Surface",
+ "subsection": "Sub Attack",
+ "risk_level": 4,
+ "weight": 90,
+ "passed_findings": 20,
+ "total_findings": 30,
+ "description": "Lower risk",
+ },
+ ]
+ assert attrs["critical_requirements"] == expected_critical
+
+ def test_overview_threatscore_weight_fallback_to_requirements(
+ self, authenticated_client, tenants_fixture, providers_fixture
+ ):
+ tenant = tenants_fixture[0]
+ provider1, provider2, *_ = providers_fixture
+
+ scan1 = self._create_scan(tenant, provider1, "fallback-scan-1")
+ scan2 = self._create_scan(tenant, provider2, "fallback-scan-2")
+
+ self._create_threatscore_snapshot(
+ tenant,
+ scan1,
+ provider1,
+ compliance_id="prowler_threatscore_aws",
+ overall_score="90.00",
+ score_delta="4.00",
+ section_scores={"1. IAM": "90.00"},
+ critical_requirements=[],
+ total_requirements=10,
+ passed_requirements=8,
+ failed_requirements=0,
+ manual_requirements=2,
+ total_findings=0,
+ passed_findings=0,
+ failed_findings=0,
+ )
+ self._create_threatscore_snapshot(
+ tenant,
+ scan2,
+ provider2,
+ compliance_id="prowler_threatscore_aws",
+ overall_score="50.00",
+ score_delta="1.00",
+ section_scores={"1. IAM": "40.00"},
+ critical_requirements=[],
+ total_requirements=12,
+ passed_requirements=5,
+ failed_requirements=7,
+ manual_requirements=0,
+ total_findings=10,
+ passed_findings=4,
+ failed_findings=6,
+ )
+
+ response = authenticated_client.get(reverse("overview-threatscore"))
+ assert response.status_code == status.HTTP_200_OK
+ aggregate = response.json()["data"][0]["attributes"]
+
+ assert Decimal(aggregate["overall_score"]) == Decimal("67.78")
+ assert Decimal(aggregate["score_delta"]) == Decimal("2.33")
+ assert aggregate["total_findings"] == 10
+ assert aggregate["total_requirements"] == 22
+ assert aggregate["manual_requirements"] == 2
+ assert aggregate["section_scores"] == {"1. IAM": "62.22"}
+
+ def test_overview_threatscore_filter_by_scan_id_returns_snapshot(
+ self, authenticated_client, tenants_fixture, providers_fixture
+ ):
+ tenant = tenants_fixture[0]
+ provider1, *_ = providers_fixture
+ scan = self._create_scan(tenant, provider1, "filter-scan")
+
+ snapshot = self._create_threatscore_snapshot(
+ tenant,
+ scan,
+ provider1,
+ compliance_id="prowler_threatscore_aws",
+ overall_score="75.00",
+ score_delta="3.00",
+ section_scores={"1. IAM": "70.00"},
+ critical_requirements=[],
+ total_requirements=50,
+ passed_requirements=30,
+ failed_requirements=20,
+ manual_requirements=0,
+ total_findings=25,
+ passed_findings=15,
+ failed_findings=10,
+ )
+
+ response = authenticated_client.get(
+ reverse("overview-threatscore"), {"filter[scan_id]": str(scan.id)}
+ )
+
+ assert response.status_code == status.HTTP_200_OK
+ body = response.json()
+ assert len(body["data"]) == 1
+ assert body["data"][0]["id"] == str(snapshot.id)
+ assert body["data"][0]["attributes"]["overall_score"] == "75.00"
+
+ def test_overview_threatscore_snapshot_id_returns_specific_snapshot(
+ self, authenticated_client, tenants_fixture, providers_fixture
+ ):
+ tenant = tenants_fixture[0]
+ provider1, *_ = providers_fixture
+ scan = self._create_scan(tenant, provider1, "snapshot-id-scan")
+
+ snapshot = self._create_threatscore_snapshot(
+ tenant,
+ scan,
+ provider1,
+ compliance_id="prowler_threatscore_aws",
+ overall_score="88.50",
+ score_delta=None,
+ section_scores={"1. IAM": "80.00"},
+ critical_requirements=[],
+ total_requirements=60,
+ passed_requirements=45,
+ failed_requirements=15,
+ manual_requirements=0,
+ total_findings=30,
+ passed_findings=25,
+ failed_findings=5,
+ )
+
+ response = authenticated_client.get(
+ reverse("overview-threatscore"), {"snapshot_id": str(snapshot.id)}
+ )
+
+ assert response.status_code == status.HTTP_200_OK
+ data = response.json()
+ assert data["data"]["id"] == str(snapshot.id)
+ assert data["data"]["attributes"]["score_delta"] is None
+
+ def test_overview_threatscore_provider_filter_returns_unaggregated_snapshot(
+ self, authenticated_client, tenants_fixture, providers_fixture
+ ):
+ tenant = tenants_fixture[0]
+ provider1, provider2, *_ = providers_fixture
+
+ scan1 = self._create_scan(tenant, provider1, "provider-filter-scan-1")
+ scan2 = self._create_scan(tenant, provider2, "provider-filter-scan-2")
+
+ snapshot1 = self._create_threatscore_snapshot(
+ tenant,
+ scan1,
+ provider1,
+ compliance_id="prowler_threatscore_aws",
+ overall_score="55.55",
+ score_delta="1.10",
+ section_scores={"1. IAM": "50.00"},
+ critical_requirements=[],
+ total_requirements=40,
+ passed_requirements=25,
+ failed_requirements=15,
+ manual_requirements=0,
+ total_findings=12,
+ passed_findings=7,
+ failed_findings=5,
+ )
+ self._create_threatscore_snapshot(
+ tenant,
+ scan2,
+ provider2,
+ compliance_id="prowler_threatscore_aws",
+ overall_score="44.44",
+ score_delta="0.80",
+ section_scores={"1. IAM": "40.00"},
+ critical_requirements=[],
+ total_requirements=30,
+ passed_requirements=18,
+ failed_requirements=12,
+ manual_requirements=0,
+ total_findings=10,
+ passed_findings=6,
+ failed_findings=4,
+ )
+
+ response = authenticated_client.get(
+ reverse("overview-threatscore"),
+ {"filter[provider_id__in]": str(provider1.id)},
+ )
+
+ assert response.status_code == status.HTTP_200_OK
+ data = response.json()["data"]
+ assert len(data) == 1
+ assert data[0]["id"] == str(snapshot1.id)
+ assert data[0]["attributes"]["overall_score"] == "55.55"
+
def test_overview_services_list_no_required_filters(
self, authenticated_client, scan_summaries_fixture
):
diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py
index c79d305942..77b4840361 100644
--- a/api/src/backend/api/v1/serializers.py
+++ b/api/src/backend/api/v1/serializers.py
@@ -47,6 +47,7 @@ from api.models import (
StatusChoices,
Task,
TenantAPIKey,
+ ThreatScoreSnapshot,
User,
UserRoleRelationship,
)
@@ -3626,3 +3627,64 @@ class MuteRuleUpdateSerializer(BaseWriteSerializer):
):
raise ValidationError("A mute rule with this name already exists.")
return value
+
+
+# ThreatScore Snapshots
+
+
+class ThreatScoreSnapshotSerializer(RLSSerializer):
+ """
+ Serializer for ThreatScore snapshots.
+ Read-only serializer for retrieving historical ThreatScore metrics.
+ """
+
+ id = serializers.SerializerMethodField()
+
+ class Meta:
+ model = ThreatScoreSnapshot
+ fields = [
+ "id",
+ "inserted_at",
+ "scan",
+ "provider",
+ "compliance_id",
+ "overall_score",
+ "score_delta",
+ "section_scores",
+ "critical_requirements",
+ "total_requirements",
+ "passed_requirements",
+ "failed_requirements",
+ "manual_requirements",
+ "total_findings",
+ "passed_findings",
+ "failed_findings",
+ ]
+ extra_kwargs = {
+ "id": {"read_only": True},
+ "inserted_at": {"read_only": True},
+ "scan": {"read_only": True},
+ "provider": {"read_only": True},
+ "compliance_id": {"read_only": True},
+ "overall_score": {"read_only": True},
+ "score_delta": {"read_only": True},
+ "section_scores": {"read_only": True},
+ "critical_requirements": {"read_only": True},
+ "total_requirements": {"read_only": True},
+ "passed_requirements": {"read_only": True},
+ "failed_requirements": {"read_only": True},
+ "manual_requirements": {"read_only": True},
+ "total_findings": {"read_only": True},
+ "passed_findings": {"read_only": True},
+ "failed_findings": {"read_only": True},
+ }
+
+ included_serializers = {
+ "scan": "api.v1.serializers.ScanIncludeSerializer",
+ "provider": "api.v1.serializers.ProviderIncludeSerializer",
+ }
+
+ def get_id(self, obj):
+ if getattr(obj, "_aggregated", False):
+ return "n/a"
+ return str(obj.id)
diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py
index 2cdbf02c8b..6724565605 100644
--- a/api/src/backend/api/v1/views.py
+++ b/api/src/backend/api/v1/views.py
@@ -3,7 +3,10 @@ import glob
import json
import logging
import os
+from collections import defaultdict
+from copy import deepcopy
from datetime import datetime, timedelta, timezone
+from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
from urllib.parse import urljoin
import sentry_sdk
@@ -24,9 +27,23 @@ from django.conf import settings as django_settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.search import SearchQuery
from django.db import transaction
-from django.db.models import Count, F, Prefetch, Q, Subquery, Sum
+from django.db.models import (
+ Case,
+ Count,
+ DecimalField,
+ ExpressionWrapper,
+ F,
+ IntegerField,
+ Max,
+ Prefetch,
+ Q,
+ Subquery,
+ Sum,
+ Value,
+ When,
+)
from django.db.models.functions import Coalesce
-from django.http import HttpResponse
+from django.http import HttpResponse, QueryDict
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.dateparse import parse_date
@@ -105,6 +122,7 @@ from api.filters import (
TaskFilter,
TenantApiKeyFilter,
TenantFilter,
+ ThreatScoreSnapshotFilter,
UserFilter,
)
from api.models import (
@@ -138,6 +156,7 @@ from api.models import (
StateChoices,
Task,
TenantAPIKey,
+ ThreatScoreSnapshot,
User,
UserRoleRelationship,
)
@@ -218,6 +237,7 @@ from api.v1.serializers import (
TenantApiKeySerializer,
TenantApiKeyUpdateSerializer,
TenantSerializer,
+ ThreatScoreSnapshotSerializer,
TokenRefreshSerializer,
TokenSerializer,
TokenSocialLoginSerializer,
@@ -3770,6 +3790,8 @@ class OverviewViewSet(BaseRLSViewSet):
return OverviewSeveritySerializer
elif self.action == "services":
return OverviewServiceSerializer
+ elif self.action == "threatscore":
+ return ThreatScoreSnapshotSerializer
return super().get_serializer_class()
def get_filterset_class(self):
@@ -4011,6 +4033,332 @@ class OverviewViewSet(BaseRLSViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
+ @extend_schema(
+ summary="Get ThreatScore snapshots",
+ description=(
+ "Retrieve ThreatScore metrics. By default, returns the latest snapshot for each provider. "
+ "Use snapshot_id to retrieve a specific historical snapshot."
+ ),
+ tags=["Overviews"],
+ parameters=[
+ OpenApiParameter(
+ name="snapshot_id",
+ type=OpenApiTypes.UUID,
+ location=OpenApiParameter.QUERY,
+ description="Retrieve a specific snapshot by ID. If not provided, returns latest snapshots.",
+ ),
+ OpenApiParameter(
+ name="provider_id",
+ type=OpenApiTypes.UUID,
+ location=OpenApiParameter.QUERY,
+ description="Filter by specific provider ID",
+ ),
+ OpenApiParameter(
+ name="provider_id__in",
+ type=OpenApiTypes.STR,
+ location=OpenApiParameter.QUERY,
+ description="Filter by multiple provider IDs (comma-separated UUIDs)",
+ ),
+ OpenApiParameter(
+ name="provider_type",
+ type=OpenApiTypes.STR,
+ location=OpenApiParameter.QUERY,
+ description="Filter by provider type (aws, azure, gcp, etc.)",
+ ),
+ OpenApiParameter(
+ name="provider_type__in",
+ type=OpenApiTypes.STR,
+ location=OpenApiParameter.QUERY,
+ description="Filter by multiple provider types (comma-separated)",
+ ),
+ ],
+ )
+ @action(detail=False, methods=["get"], url_name="threatscore")
+ def threatscore(self, request):
+ """
+ Get ThreatScore snapshots.
+
+ Default behavior: Returns the latest snapshot for each provider.
+ With snapshot_id: Returns the specific snapshot requested.
+ """
+ tenant_id = self.request.tenant_id
+ snapshot_id = request.query_params.get("snapshot_id")
+
+ # Base queryset with RLS
+ base_queryset = ThreatScoreSnapshot.objects.filter(tenant_id=tenant_id)
+
+ # Apply RBAC filtering
+ if hasattr(self, "allowed_providers"):
+ base_queryset = base_queryset.filter(provider__in=self.allowed_providers)
+
+ # Case 1: Specific snapshot requested
+ if snapshot_id:
+ try:
+ snapshot = base_queryset.get(id=snapshot_id)
+ serializer = ThreatScoreSnapshotSerializer(
+ snapshot, context={"request": request}
+ )
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ except ThreatScoreSnapshot.DoesNotExist:
+ raise NotFound(detail="ThreatScore snapshot not found")
+
+ # Case 2: Latest snapshot per provider (default)
+ # Apply filters manually: this @action is outside the standard list endpoint flow,
+ # so DRF's filter backends don't execute and we must flatten JSON:API params ourselves.
+ normalized_params = QueryDict(mutable=True)
+ for param_key, values in request.query_params.lists():
+ normalized_key = param_key
+ if param_key.startswith("filter[") and param_key.endswith("]"):
+ normalized_key = param_key[7:-1]
+ if normalized_key == "snapshot_id":
+ continue
+ normalized_params.setlist(normalized_key, values)
+
+ filterset = ThreatScoreSnapshotFilter(normalized_params, queryset=base_queryset)
+ filtered_queryset = filterset.qs
+
+ # Get distinct provider IDs from filtered queryset
+ # Pick the latest snapshot per provider using Postgres DISTINCT ON pattern.
+ # This avoids issuing one query per provider (N+1) when the filtered dataset is large.
+ latest_snapshot_ids = list(
+ filtered_queryset.order_by("provider_id", "-inserted_at")
+ .distinct("provider_id")
+ .values_list("id", flat=True)
+ )
+ latest_snapshot_map = {
+ snapshot.id: snapshot
+ for snapshot in filtered_queryset.filter(id__in=latest_snapshot_ids)
+ }
+ latest_snapshots = [
+ latest_snapshot_map[snapshot_id]
+ for snapshot_id in latest_snapshot_ids
+ if snapshot_id in latest_snapshot_map
+ ]
+
+ if len(latest_snapshots) <= 1:
+ serializer = ThreatScoreSnapshotSerializer(
+ latest_snapshots, many=True, context={"request": request}
+ )
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ snapshot_ids = [
+ snapshot.id for snapshot in latest_snapshots if snapshot and snapshot.id
+ ]
+ aggregated_snapshot = self._build_threatscore_overview_snapshot(
+ snapshot_ids, tenant_id
+ )
+ serializer = ThreatScoreSnapshotSerializer(
+ [aggregated_snapshot], many=True, context={"request": request}
+ )
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ def _build_threatscore_overview_snapshot(self, snapshot_ids, tenant_id):
+ """
+ Aggregate the latest snapshots into a single overview snapshot for the tenant.
+ """
+ if not snapshot_ids:
+ raise ValueError(
+ "Snapshot id list cannot be empty when aggregating threatscore overview"
+ )
+
+ base_queryset = ThreatScoreSnapshot.objects.filter(
+ tenant_id=tenant_id, id__in=snapshot_ids
+ )
+
+ annotated_queryset = (
+ base_queryset.annotate(
+ active_requirements=ExpressionWrapper(
+ F("total_requirements") - F("manual_requirements"),
+ output_field=IntegerField(),
+ )
+ )
+ .annotate(
+ weight=Case(
+ When(total_findings__gt=0, then=F("total_findings")),
+ When(
+ active_requirements__gt=0,
+ then=F("active_requirements"),
+ ),
+ default=Value(1, output_field=IntegerField()),
+ output_field=IntegerField(),
+ )
+ )
+ .order_by()
+ )
+
+ aggregated_metrics = annotated_queryset.aggregate(
+ total_requirements=Sum("total_requirements"),
+ passed_requirements=Sum("passed_requirements"),
+ failed_requirements=Sum("failed_requirements"),
+ manual_requirements=Sum("manual_requirements"),
+ total_findings=Sum("total_findings"),
+ passed_findings=Sum("passed_findings"),
+ failed_findings=Sum("failed_findings"),
+ weighted_overall_sum=Sum(
+ ExpressionWrapper(
+ F("overall_score") * F("weight"),
+ output_field=DecimalField(max_digits=14, decimal_places=4),
+ )
+ ),
+ overall_weight=Sum("weight"),
+ unweighted_overall_sum=Sum("overall_score"),
+ weighted_delta_sum=Sum(
+ Case(
+ When(
+ score_delta__isnull=False,
+ then=ExpressionWrapper(
+ F("score_delta") * F("weight"),
+ output_field=DecimalField(max_digits=14, decimal_places=4),
+ ),
+ ),
+ default=Value(
+ Decimal("0"),
+ output_field=DecimalField(max_digits=14, decimal_places=4),
+ ),
+ output_field=DecimalField(max_digits=14, decimal_places=4),
+ )
+ ),
+ delta_weight=Sum(
+ Case(
+ When(score_delta__isnull=False, then=F("weight")),
+ default=Value(0, output_field=IntegerField()),
+ output_field=IntegerField(),
+ )
+ ),
+ provider_count=Count("id"),
+ latest_inserted_at=Max("inserted_at"),
+ )
+
+ total_requirements = aggregated_metrics["total_requirements"] or 0
+ passed_requirements = aggregated_metrics["passed_requirements"] or 0
+ failed_requirements = aggregated_metrics["failed_requirements"] or 0
+ manual_requirements = aggregated_metrics["manual_requirements"] or 0
+ total_findings = aggregated_metrics["total_findings"] or 0
+ passed_findings = aggregated_metrics["passed_findings"] or 0
+ failed_findings = aggregated_metrics["failed_findings"] or 0
+
+ weighted_overall_sum = aggregated_metrics["weighted_overall_sum"]
+ if weighted_overall_sum is None:
+ weighted_overall_sum = Decimal("0")
+ unweighted_overall_sum = aggregated_metrics["unweighted_overall_sum"]
+ if unweighted_overall_sum is None:
+ unweighted_overall_sum = Decimal("0")
+
+ overall_weight = aggregated_metrics["overall_weight"] or 0
+ provider_count = aggregated_metrics["provider_count"] or 0
+
+ weighted_delta_sum = aggregated_metrics["weighted_delta_sum"]
+ if weighted_delta_sum is None:
+ weighted_delta_sum = Decimal("0")
+ delta_weight = aggregated_metrics["delta_weight"] or 0
+
+ if overall_weight > 0:
+ overall_score = (weighted_overall_sum / Decimal(overall_weight)).quantize(
+ Decimal("0.01"), rounding=ROUND_HALF_UP
+ )
+ elif provider_count > 0:
+ overall_score = (unweighted_overall_sum / Decimal(provider_count)).quantize(
+ Decimal("0.01"), rounding=ROUND_HALF_UP
+ )
+ else:
+ overall_score = Decimal("0.00")
+
+ if delta_weight > 0:
+ score_delta = (weighted_delta_sum / Decimal(delta_weight)).quantize(
+ Decimal("0.01"), rounding=ROUND_HALF_UP
+ )
+ else:
+ score_delta = None
+
+ section_weighted_sums = defaultdict(lambda: Decimal("0"))
+ section_weights = defaultdict(lambda: Decimal("0"))
+
+ combined_critical_requirements = {}
+
+ snapshots_with_weight = list(annotated_queryset)
+
+ for snapshot in snapshots_with_weight:
+ weight_value = getattr(snapshot, "weight", None)
+ try:
+ weight_decimal = Decimal(weight_value)
+ except (InvalidOperation, TypeError):
+ weight_decimal = Decimal("1")
+ if weight_decimal <= 0:
+ weight_decimal = Decimal("1")
+
+ section_scores = snapshot.section_scores or {}
+ for section, score in section_scores.items():
+ try:
+ score_decimal = Decimal(str(score))
+ except (InvalidOperation, TypeError):
+ continue
+ section_weighted_sums[section] += score_decimal * weight_decimal
+ section_weights[section] += weight_decimal
+
+ for requirement in snapshot.critical_requirements or []:
+ key = requirement.get("requirement_id") or requirement.get("title")
+ if not key:
+ continue
+ existing = combined_critical_requirements.get(key)
+
+ def requirement_sort_key(item):
+ return (
+ item.get("risk_level") or 0,
+ item.get("weight") or 0,
+ )
+
+ if existing is None or requirement_sort_key(
+ requirement
+ ) > requirement_sort_key(existing):
+ combined_critical_requirements[key] = deepcopy(requirement)
+
+ aggregated_section_scores = {}
+ for section, total in section_weighted_sums.items():
+ weight_total = section_weights[section]
+ if weight_total > 0:
+ aggregated_section_scores[section] = str(
+ (total / weight_total).quantize(
+ Decimal("0.01"), rounding=ROUND_HALF_UP
+ )
+ )
+
+ aggregated_section_scores = dict(sorted(aggregated_section_scores.items()))
+
+ aggregated_critical_requirements = sorted(
+ combined_critical_requirements.values(),
+ key=lambda item: (
+ item.get("risk_level") or 0,
+ item.get("weight") or 0,
+ ),
+ reverse=True,
+ )
+
+ aggregated_snapshot = ThreatScoreSnapshot(
+ tenant_id=tenant_id,
+ scan=None,
+ provider=None,
+ compliance_id="prowler_threatscore_overview",
+ overall_score=overall_score,
+ score_delta=score_delta,
+ section_scores=aggregated_section_scores,
+ critical_requirements=aggregated_critical_requirements,
+ total_requirements=total_requirements,
+ passed_requirements=passed_requirements,
+ failed_requirements=failed_requirements,
+ manual_requirements=manual_requirements,
+ total_findings=total_findings,
+ passed_findings=passed_findings,
+ failed_findings=failed_findings,
+ )
+
+ latest_inserted_at = aggregated_metrics["latest_inserted_at"]
+ if latest_inserted_at is not None:
+ aggregated_snapshot.inserted_at = latest_inserted_at
+
+ aggregated_snapshot._aggregated = True
+
+ return aggregated_snapshot
+
@extend_schema(tags=["Schedule"])
@extend_schema_view(
diff --git a/api/src/backend/tasks/jobs/report.py b/api/src/backend/tasks/jobs/report.py
index 641fa5757a..4136cd3748 100644
--- a/api/src/backend/tasks/jobs/report.py
+++ b/api/src/backend/tasks/jobs/report.py
@@ -7,7 +7,6 @@ from shutil import rmtree
import matplotlib.pyplot as plt
from celery.utils.log import get_task_logger
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE, DJANGO_TMP_OUTPUT_DIRECTORY
-from django.db.models import Count, Q
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER
from reportlab.lib.pagesizes import letter
@@ -26,11 +25,22 @@ from reportlab.platypus import (
TableStyle,
)
from tasks.jobs.export import _generate_output_directory, _upload_to_s3
+from tasks.jobs.threatscore import compute_threatscore_metrics
+from tasks.jobs.threatscore_utils import (
+ _aggregate_requirement_statistics_from_database,
+ _calculate_requirements_data_from_statistics,
+)
from tasks.utils import batched
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
-from api.models import Finding, Provider, ScanSummary, StatusChoices
+from api.models import (
+ Finding,
+ Provider,
+ ScanSummary,
+ StatusChoices,
+ ThreatScoreSnapshot,
+)
from api.utils import initialize_prowler_provider
from prowler.lib.check.compliance_models import Compliance
from prowler.lib.outputs.finding import Finding as FindingOutput
@@ -434,56 +444,6 @@ def _add_pdf_footer(canvas_obj: canvas.Canvas, doc: SimpleDocTemplate) -> None:
canvas_obj.drawString(width - text_width - 30, 20, powered_text)
-def _aggregate_requirement_statistics_from_database(
- tenant_id: str, scan_id: str
-) -> dict[str, dict[str, int]]:
- """
- Aggregate finding statistics by check_id using database aggregation.
-
- This function uses Django ORM aggregation to calculate pass/fail statistics
- entirely in the database, avoiding the need to load findings into memory.
-
- Args:
- tenant_id (str): The tenant ID for Row-Level Security context.
- scan_id (str): The ID of the scan to retrieve findings for.
-
- Returns:
- dict[str, dict[str, int]]: Dictionary mapping check_id to statistics:
- - 'passed' (int): Number of passed findings for this check
- - 'total' (int): Total number of findings for this check
-
- Example:
- {
- 'aws_iam_user_mfa_enabled': {'passed': 10, 'total': 15},
- 'aws_s3_bucket_public_access': {'passed': 0, 'total': 5}
- }
- """
- requirement_statistics_by_check_id = {}
-
- with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
- # Use database aggregation to calculate stats without loading findings into memory
- aggregated_statistics_queryset = (
- Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id)
- .values("check_id")
- .annotate(
- total_findings=Count("id"),
- passed_findings=Count("id", filter=Q(status=StatusChoices.PASS)),
- )
- )
-
- for aggregated_stat in aggregated_statistics_queryset:
- check_id = aggregated_stat["check_id"]
- requirement_statistics_by_check_id[check_id] = {
- "passed": aggregated_stat["passed_findings"],
- "total": aggregated_stat["total_findings"],
- }
-
- logger.info(
- f"Aggregated statistics for {len(requirement_statistics_by_check_id)} unique checks"
- )
- return requirement_statistics_by_check_id
-
-
def _load_findings_for_requirement_checks(
tenant_id: str, scan_id: str, check_ids: list[str], prowler_provider
) -> dict[str, list[FindingOutput]]:
@@ -544,84 +504,6 @@ def _load_findings_for_requirement_checks(
return dict(findings_by_check_id)
-def _calculate_requirements_data_from_statistics(
- compliance_obj, requirement_statistics_by_check_id: dict[str, dict[str, int]]
-) -> tuple[dict[str, dict], list[dict]]:
- """
- Calculate requirement status and statistics using pre-aggregated database statistics.
-
- This function uses O(n) lookups with pre-aggregated statistics from the database,
- avoiding the need to iterate over all findings for each requirement.
-
- Args:
- compliance_obj: The compliance framework object containing requirements.
- requirement_statistics_by_check_id (dict[str, dict[str, int]]): Pre-aggregated statistics
- mapping check_id to {'passed': int, 'total': int} counts.
-
- Returns:
- tuple[dict[str, dict], list[dict]]: A tuple containing:
- - attributes_by_requirement_id: Dictionary mapping requirement IDs to their attributes.
- - requirements_list: List of requirement dictionaries with status and statistics.
- """
- attributes_by_requirement_id = {}
- requirements_list = []
-
- compliance_framework = getattr(compliance_obj, "Framework", "N/A")
- compliance_version = getattr(compliance_obj, "Version", "N/A")
-
- for requirement in compliance_obj.Requirements:
- requirement_id = requirement.Id
- requirement_description = getattr(requirement, "Description", "")
- requirement_checks = getattr(requirement, "Checks", [])
- requirement_attributes = getattr(requirement, "Attributes", [])
-
- # Store requirement metadata for later use
- attributes_by_requirement_id[requirement_id] = {
- "attributes": {
- "req_attributes": requirement_attributes,
- "checks": requirement_checks,
- },
- "description": requirement_description,
- }
-
- # Calculate aggregated passed and total findings for this requirement
- total_passed_findings = 0
- total_findings_count = 0
-
- for check_id in requirement_checks:
- if check_id in requirement_statistics_by_check_id:
- check_statistics = requirement_statistics_by_check_id[check_id]
- total_findings_count += check_statistics["total"]
- total_passed_findings += check_statistics["passed"]
-
- # Determine overall requirement status based on findings
- if total_findings_count > 0:
- if total_passed_findings == total_findings_count:
- requirement_status = StatusChoices.PASS
- else:
- # Partial pass or complete fail both count as FAIL
- requirement_status = StatusChoices.FAIL
- else:
- # No findings means manual review required
- requirement_status = StatusChoices.MANUAL
-
- requirements_list.append(
- {
- "id": requirement_id,
- "attributes": {
- "framework": compliance_framework,
- "version": compliance_version,
- "status": requirement_status,
- "description": requirement_description,
- "passed_findings": total_passed_findings,
- "total_findings": total_findings_count,
- },
- }
- )
-
- return attributes_by_requirement_id, requirements_list
-
-
def generate_threatscore_report(
tenant_id: str,
scan_id: str,
@@ -1262,8 +1144,9 @@ def generate_threatscore_report_job(
2. Checks provider type compatibility
3. Generates the output directory
4. Calls generate_threatscore_report to create the PDF
- 5. Uploads the PDF to S3
- 6. Cleans up temporary files
+ 5. Computes and stores ThreatScore metrics snapshot
+ 6. Uploads the PDF to S3
+ 7. Cleans up temporary files
Args:
tenant_id (str): The tenant ID for Row-Level Security context.
@@ -1317,6 +1200,66 @@ def generate_threatscore_report_job(
min_risk_level=4,
)
+ # Compute and store ThreatScore metrics snapshot
+ logger.info(f"Computing ThreatScore metrics for scan {scan_id}")
+ try:
+ metrics = compute_threatscore_metrics(
+ tenant_id=tenant_id,
+ scan_id=scan_id,
+ provider_id=provider_id,
+ compliance_id=compliance_id,
+ min_risk_level=4,
+ )
+
+ # Create snapshot in database
+ with rls_transaction(tenant_id):
+ # Get previous snapshot for the same provider to calculate delta
+ previous_snapshot = (
+ ThreatScoreSnapshot.objects.filter(
+ tenant_id=tenant_id,
+ provider_id=provider_id,
+ compliance_id=compliance_id,
+ )
+ .order_by("-inserted_at")
+ .first()
+ )
+
+ # Calculate score delta (improvement)
+ score_delta = None
+ if previous_snapshot:
+ score_delta = metrics["overall_score"] - float(
+ previous_snapshot.overall_score
+ )
+
+ snapshot = ThreatScoreSnapshot.objects.create(
+ tenant_id=tenant_id,
+ scan_id=scan_id,
+ provider_id=provider_id,
+ compliance_id=compliance_id,
+ overall_score=metrics["overall_score"],
+ score_delta=score_delta,
+ section_scores=metrics["section_scores"],
+ critical_requirements=metrics["critical_requirements"],
+ total_requirements=metrics["total_requirements"],
+ passed_requirements=metrics["passed_requirements"],
+ failed_requirements=metrics["failed_requirements"],
+ manual_requirements=metrics["manual_requirements"],
+ total_findings=metrics["total_findings"],
+ passed_findings=metrics["passed_findings"],
+ failed_findings=metrics["failed_findings"],
+ )
+
+ delta_msg = (
+ f" (delta: {score_delta:+.2f}%)" if score_delta is not None else ""
+ )
+ logger.info(
+ f"ThreatScore snapshot created with ID {snapshot.id} "
+ f"(score: {snapshot.overall_score}%{delta_msg})"
+ )
+ except Exception as e:
+ # Log error but don't fail the job if snapshot creation fails
+ logger.error(f"Error creating ThreatScore snapshot: {e}")
+
upload_uri = _upload_to_s3(
tenant_id,
scan_id,
diff --git a/api/src/backend/tasks/jobs/threatscore.py b/api/src/backend/tasks/jobs/threatscore.py
new file mode 100644
index 0000000000..414f2d20f2
--- /dev/null
+++ b/api/src/backend/tasks/jobs/threatscore.py
@@ -0,0 +1,214 @@
+from celery.utils.log import get_task_logger
+from tasks.jobs.threatscore_utils import (
+ _aggregate_requirement_statistics_from_database,
+ _calculate_requirements_data_from_statistics,
+)
+
+from api.db_router import READ_REPLICA_ALIAS
+from api.db_utils import rls_transaction
+from api.models import Provider, StatusChoices
+from prowler.lib.check.compliance_models import Compliance
+
+logger = get_task_logger(__name__)
+
+
+def compute_threatscore_metrics(
+ tenant_id: str,
+ scan_id: str,
+ provider_id: str,
+ compliance_id: str,
+ min_risk_level: int = 4,
+) -> dict:
+ """
+ Compute ThreatScore metrics for a given scan.
+
+ This function calculates all the metrics needed for a ThreatScore snapshot:
+ - Overall ThreatScore percentage
+ - Section-by-section scores
+ - Critical failed requirements (risk >= min_risk_level)
+ - Summary statistics (requirements and findings counts)
+
+ Args:
+ tenant_id (str): The tenant ID for Row-Level Security context.
+ scan_id (str): The ID of the scan to analyze.
+ provider_id (str): The ID of the provider used in the scan.
+ compliance_id (str): Compliance framework ID (e.g., "prowler_threatscore_aws").
+ min_risk_level (int): Minimum risk level for critical requirements. Defaults to 4.
+
+ Returns:
+ dict: A dictionary containing:
+ - overall_score (float): Overall ThreatScore percentage (0-100)
+ - section_scores (dict): Section name -> score percentage mapping
+ - critical_requirements (list): List of critical failed requirement dicts
+ - total_requirements (int): Total number of requirements
+ - passed_requirements (int): Number of PASS requirements
+ - failed_requirements (int): Number of FAIL requirements
+ - manual_requirements (int): Number of MANUAL requirements
+ - total_findings (int): Total findings count
+ - passed_findings (int): Passed findings count
+ - failed_findings (int): Failed findings count
+
+ Example:
+ >>> metrics = compute_threatscore_metrics(
+ ... tenant_id="tenant-123",
+ ... scan_id="scan-456",
+ ... provider_id="provider-789",
+ ... compliance_id="prowler_threatscore_aws"
+ ... )
+ >>> print(f"Overall ThreatScore: {metrics['overall_score']:.2f}%")
+ """
+ # Get provider and compliance information
+ with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
+ provider_obj = Provider.objects.get(id=provider_id)
+ provider_type = provider_obj.provider
+
+ frameworks_bulk = Compliance.get_bulk(provider_type)
+ compliance_obj = frameworks_bulk[compliance_id]
+
+ # Aggregate requirement statistics from database
+ requirement_statistics_by_check_id = (
+ _aggregate_requirement_statistics_from_database(tenant_id, scan_id)
+ )
+
+ # Calculate requirements data using aggregated statistics
+ attributes_by_requirement_id, requirements_list = (
+ _calculate_requirements_data_from_statistics(
+ compliance_obj, requirement_statistics_by_check_id
+ )
+ )
+
+ # Initialize metrics
+ overall_numerator = 0
+ overall_denominator = 0
+ overall_has_findings = False
+
+ sections_data = {}
+
+ total_requirements = len(requirements_list)
+ passed_requirements = 0
+ failed_requirements = 0
+ manual_requirements = 0
+ total_findings = 0
+ passed_findings = 0
+ failed_findings = 0
+
+ critical_requirements_list = []
+
+ # Process each requirement
+ for requirement in requirements_list:
+ requirement_id = requirement["id"]
+ requirement_status = requirement["attributes"]["status"]
+ requirement_attributes = attributes_by_requirement_id.get(requirement_id, {})
+
+ # Count requirements by status
+ if requirement_status == StatusChoices.PASS:
+ passed_requirements += 1
+ elif requirement_status == StatusChoices.FAIL:
+ failed_requirements += 1
+ elif requirement_status == StatusChoices.MANUAL:
+ manual_requirements += 1
+
+ # Get findings data
+ req_passed_findings = requirement["attributes"].get("passed_findings", 0)
+ req_total_findings = requirement["attributes"].get("total_findings", 0)
+
+ # Accumulate findings counts
+ total_findings += req_total_findings
+ passed_findings += req_passed_findings
+ failed_findings += req_total_findings - req_passed_findings
+
+ # Skip requirements with no findings
+ if req_total_findings == 0:
+ continue
+
+ overall_has_findings = True
+
+ # Get requirement metadata
+ metadata = requirement_attributes.get("attributes", {}).get(
+ "req_attributes", []
+ )
+ if not metadata or len(metadata) == 0:
+ continue
+
+ m = metadata[0]
+ risk_level = getattr(m, "LevelOfRisk", 0)
+ weight = getattr(m, "Weight", 0)
+ section = getattr(m, "Section", "Unknown")
+
+ # Calculate ThreatScore components using formula from UI
+ rate_i = req_passed_findings / req_total_findings
+ rfac_i = 1 + 0.25 * risk_level
+
+ # Update overall score
+ overall_numerator += rate_i * req_total_findings * weight * rfac_i
+ overall_denominator += req_total_findings * weight * rfac_i
+
+ # Update section scores
+ if section not in sections_data:
+ sections_data[section] = {
+ "numerator": 0,
+ "denominator": 0,
+ "has_findings": False,
+ }
+
+ sections_data[section]["has_findings"] = True
+ sections_data[section]["numerator"] += (
+ rate_i * req_total_findings * weight * rfac_i
+ )
+ sections_data[section]["denominator"] += req_total_findings * weight * rfac_i
+
+ # Identify critical failed requirements
+ if requirement_status == StatusChoices.FAIL and risk_level >= min_risk_level:
+ critical_requirements_list.append(
+ {
+ "requirement_id": requirement_id,
+ "title": getattr(m, "Title", "N/A"),
+ "section": section,
+ "subsection": getattr(m, "SubSection", "N/A"),
+ "risk_level": risk_level,
+ "weight": weight,
+ "passed_findings": req_passed_findings,
+ "total_findings": req_total_findings,
+ "description": getattr(m, "AttributeDescription", "N/A"),
+ }
+ )
+
+ # Calculate overall ThreatScore
+ if not overall_has_findings:
+ overall_score = 100.0
+ elif overall_denominator > 0:
+ overall_score = (overall_numerator / overall_denominator) * 100
+ else:
+ overall_score = 0.0
+
+ # Calculate section scores
+ section_scores = {}
+ for section, data in sections_data.items():
+ if data["has_findings"] and data["denominator"] > 0:
+ section_scores[section] = (data["numerator"] / data["denominator"]) * 100
+ else:
+ section_scores[section] = 100.0
+
+ # Sort critical requirements by risk level (desc) and weight (desc)
+ critical_requirements_list.sort(
+ key=lambda x: (x["risk_level"], x["weight"]), reverse=True
+ )
+
+ logger.info(
+ f"ThreatScore computed: {overall_score:.2f}% "
+ f"({passed_requirements}/{total_requirements} requirements passed, "
+ f"{len(critical_requirements_list)} critical failures)"
+ )
+
+ return {
+ "overall_score": round(overall_score, 2),
+ "section_scores": {k: round(v, 2) for k, v in section_scores.items()},
+ "critical_requirements": critical_requirements_list,
+ "total_requirements": total_requirements,
+ "passed_requirements": passed_requirements,
+ "failed_requirements": failed_requirements,
+ "manual_requirements": manual_requirements,
+ "total_findings": total_findings,
+ "passed_findings": passed_findings,
+ "failed_findings": failed_findings,
+ }
diff --git a/api/src/backend/tasks/jobs/threatscore_utils.py b/api/src/backend/tasks/jobs/threatscore_utils.py
new file mode 100644
index 0000000000..bc2cb009a2
--- /dev/null
+++ b/api/src/backend/tasks/jobs/threatscore_utils.py
@@ -0,0 +1,127 @@
+from celery.utils.log import get_task_logger
+from django.db.models import Count, Q
+
+from api.db_router import READ_REPLICA_ALIAS
+from api.db_utils import rls_transaction
+from api.models import Finding, StatusChoices
+
+logger = get_task_logger(__name__)
+
+
+def _aggregate_requirement_statistics_from_database(
+ tenant_id: str, scan_id: str
+) -> dict[str, dict[str, int]]:
+ """
+ Aggregate finding statistics by check_id using database aggregation.
+
+ This function uses Django ORM aggregation to calculate pass/fail statistics
+ entirely in the database, avoiding the need to load findings into memory.
+
+ Args:
+ tenant_id (str): The tenant ID for Row-Level Security context.
+ scan_id (str): The ID of the scan to retrieve findings for.
+
+ Returns:
+ dict[str, dict[str, int]]: Dictionary mapping check_id to statistics:
+ - 'passed' (int): Number of passed findings for this check
+ - 'total' (int): Total number of findings for this check
+
+ Example:
+ {
+ 'aws_iam_user_mfa_enabled': {'passed': 10, 'total': 15},
+ 'aws_s3_bucket_public_access': {'passed': 0, 'total': 5}
+ }
+ """
+ requirement_statistics_by_check_id = {}
+
+ with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
+ aggregated_statistics_queryset = (
+ Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id)
+ .values("check_id")
+ .annotate(
+ total_findings=Count("id"),
+ passed_findings=Count("id", filter=Q(status=StatusChoices.PASS)),
+ )
+ )
+
+ for aggregated_stat in aggregated_statistics_queryset:
+ check_id = aggregated_stat["check_id"]
+ requirement_statistics_by_check_id[check_id] = {
+ "passed": aggregated_stat["passed_findings"],
+ "total": aggregated_stat["total_findings"],
+ }
+
+ logger.info(
+ f"Aggregated statistics for {len(requirement_statistics_by_check_id)} unique checks"
+ )
+ return requirement_statistics_by_check_id
+
+
+def _calculate_requirements_data_from_statistics(
+ compliance_obj, requirement_statistics_by_check_id: dict[str, dict[str, int]]
+) -> tuple[dict[str, dict], list[dict]]:
+ """
+ Calculate requirement status and statistics using pre-aggregated database statistics.
+
+ Args:
+ compliance_obj: The compliance framework object containing requirements.
+ requirement_statistics_by_check_id (dict[str, dict[str, int]]): Pre-aggregated statistics
+ mapping check_id to {'passed': int, 'total': int} counts.
+
+ Returns:
+ tuple[dict[str, dict], list[dict]]: A tuple containing:
+ - attributes_by_requirement_id: Dictionary mapping requirement IDs to their attributes.
+ - requirements_list: List of requirement dictionaries with status and statistics.
+ """
+ attributes_by_requirement_id = {}
+ requirements_list = []
+
+ compliance_framework = getattr(compliance_obj, "Framework", "N/A")
+ compliance_version = getattr(compliance_obj, "Version", "N/A")
+
+ for requirement in compliance_obj.Requirements:
+ requirement_id = requirement.Id
+ requirement_description = getattr(requirement, "Description", "")
+ requirement_checks = getattr(requirement, "Checks", [])
+ requirement_attributes = getattr(requirement, "Attributes", [])
+
+ attributes_by_requirement_id[requirement_id] = {
+ "attributes": {
+ "req_attributes": requirement_attributes,
+ "checks": requirement_checks,
+ },
+ "description": requirement_description,
+ }
+
+ total_passed_findings = 0
+ total_findings_count = 0
+
+ for check_id in requirement_checks:
+ if check_id in requirement_statistics_by_check_id:
+ check_statistics = requirement_statistics_by_check_id[check_id]
+ total_findings_count += check_statistics["total"]
+ total_passed_findings += check_statistics["passed"]
+
+ if total_findings_count > 0:
+ if total_passed_findings == total_findings_count:
+ requirement_status = StatusChoices.PASS
+ else:
+ requirement_status = StatusChoices.FAIL
+ else:
+ requirement_status = StatusChoices.MANUAL
+
+ requirements_list.append(
+ {
+ "id": requirement_id,
+ "attributes": {
+ "framework": compliance_framework,
+ "version": compliance_version,
+ "status": requirement_status,
+ "description": requirement_description,
+ "passed_findings": total_passed_findings,
+ "total_findings": total_findings_count,
+ },
+ }
+ )
+
+ return attributes_by_requirement_id, requirements_list
diff --git a/api/src/backend/tasks/tests/test_report.py b/api/src/backend/tasks/tests/test_report.py
index a472067b2d..91d3b1145d 100644
--- a/api/src/backend/tasks/tests/test_report.py
+++ b/api/src/backend/tasks/tests/test_report.py
@@ -1,19 +1,25 @@
import uuid
+from datetime import timedelta
+from decimal import Decimal
from pathlib import Path
from unittest.mock import MagicMock, patch
import matplotlib
import pytest
+from django.utils import timezone
+from freezegun import freeze_time
from tasks.jobs.report import (
- _aggregate_requirement_statistics_from_database,
- _calculate_requirements_data_from_statistics,
_load_findings_for_requirement_checks,
generate_threatscore_report,
generate_threatscore_report_job,
)
+from tasks.jobs.threatscore_utils import (
+ _aggregate_requirement_statistics_from_database,
+ _calculate_requirements_data_from_statistics,
+)
from tasks.tasks import generate_threatscore_report_task
-from api.models import Finding, StatusChoices
+from api.models import Finding, Scan, StateChoices, StatusChoices, ThreatScoreSnapshot
from prowler.lib.check.models import Severity
matplotlib.use("Agg") # Use non-interactive backend for tests
@@ -39,6 +45,7 @@ class TestGenerateThreatscoreReport:
assert result == {"upload": False}
mock_filter.assert_called_once_with(scan_id=self.scan_id)
+ @patch("tasks.jobs.report.ThreatScoreSnapshot.objects.create")
@patch("tasks.jobs.report.rmtree")
@patch("tasks.jobs.report._upload_to_s3")
@patch("tasks.jobs.report.generate_threatscore_report")
@@ -53,6 +60,7 @@ class TestGenerateThreatscoreReport:
mock_generate_report,
mock_upload,
mock_rmtree,
+ mock_snapshot_create,
):
mock_scan_summary_filter.return_value.exists.return_value = True
@@ -95,8 +103,10 @@ class TestGenerateThreatscoreReport:
Path("/tmp/threatscore_path_threatscore_report.pdf").parent,
ignore_errors=True,
)
+ mock_snapshot_create.assert_called_once()
- def test_generate_threatscore_report_fails_upload(self):
+ @patch("tasks.jobs.report.ThreatScoreSnapshot.objects.create")
+ def test_generate_threatscore_report_fails_upload(self, mock_snapshot_create):
with (
patch("tasks.jobs.report.ScanSummary.objects.filter") as mock_filter,
patch("tasks.jobs.report.Provider.objects.get") as mock_provider_get,
@@ -125,8 +135,12 @@ class TestGenerateThreatscoreReport:
)
assert result == {"upload": False}
+ mock_snapshot_create.assert_called_once()
- def test_generate_threatscore_report_logs_rmtree_exception(self, caplog):
+ @patch("tasks.jobs.report.ThreatScoreSnapshot.objects.create")
+ def test_generate_threatscore_report_logs_rmtree_exception(
+ self, mock_snapshot_create, caplog
+ ):
with (
patch("tasks.jobs.report.ScanSummary.objects.filter") as mock_filter,
patch("tasks.jobs.report.Provider.objects.get") as mock_provider_get,
@@ -160,8 +174,10 @@ class TestGenerateThreatscoreReport:
provider_id=self.provider_id,
)
assert "Error deleting output files" in caplog.text
+ mock_snapshot_create.assert_called_once()
- def test_generate_threatscore_report_azure_provider(self):
+ @patch("tasks.jobs.report.ThreatScoreSnapshot.objects.create")
+ def test_generate_threatscore_report_azure_provider(self, mock_snapshot_create):
with (
patch("tasks.jobs.report.ScanSummary.objects.filter") as mock_filter,
patch("tasks.jobs.report.Provider.objects.get") as mock_provider_get,
@@ -200,6 +216,135 @@ class TestGenerateThreatscoreReport:
only_failed=True,
min_risk_level=4,
)
+ mock_snapshot_create.assert_called_once()
+
+ @patch("tasks.jobs.report.rmtree")
+ @patch(
+ "tasks.jobs.report._upload_to_s3",
+ return_value="s3://bucket/threatscore/threatscore_report.pdf",
+ )
+ @patch("tasks.jobs.report.generate_threatscore_report")
+ @patch("tasks.jobs.report._generate_output_directory")
+ @patch("tasks.jobs.report.ScanSummary.objects.filter")
+ @patch("tasks.jobs.report.compute_threatscore_metrics")
+ @pytest.mark.django_db
+ @freeze_time("2025-01-10T12:00:00Z")
+ def test_generate_threatscore_report_persists_snapshot_and_delta(
+ self,
+ mock_compute_metrics,
+ mock_scan_summary_filter,
+ mock_generate_output_directory,
+ mock_generate_report,
+ mock_upload,
+ mock_rmtree,
+ tenants_fixture,
+ providers_fixture,
+ ):
+ tenant = tenants_fixture[0]
+ provider = providers_fixture[0]
+
+ scan_previous = Scan.objects.create(
+ tenant=tenant,
+ provider=provider,
+ name="previous-threatscore-scan",
+ trigger=Scan.TriggerChoices.MANUAL,
+ state=StateChoices.COMPLETED,
+ started_at=timezone.now() - timedelta(hours=4),
+ completed_at=timezone.now() - timedelta(hours=3),
+ )
+ ThreatScoreSnapshot.objects.create(
+ tenant=tenant,
+ scan=scan_previous,
+ provider=provider,
+ compliance_id="prowler_threatscore_aws",
+ overall_score=Decimal("70.00"),
+ score_delta=None,
+ section_scores={"1. IAM": "65.00"},
+ critical_requirements=[],
+ total_requirements=50,
+ passed_requirements=35,
+ failed_requirements=15,
+ manual_requirements=0,
+ total_findings=40,
+ passed_findings=25,
+ failed_findings=15,
+ )
+
+ scan_current = Scan.objects.create(
+ tenant=tenant,
+ provider=provider,
+ name="current-threatscore-scan",
+ trigger=Scan.TriggerChoices.MANUAL,
+ state=StateChoices.COMPLETED,
+ started_at=timezone.now() - timedelta(hours=2),
+ completed_at=timezone.now() - timedelta(hours=1),
+ )
+
+ mock_scan_summary_filter.return_value.exists.return_value = True
+ mock_generate_output_directory.return_value = (
+ "/tmp/output",
+ "/tmp/compressed",
+ "/tmp/threatscore_path",
+ )
+
+ metrics = {
+ "overall_score": 85.5,
+ "score_delta": 10.0,
+ "section_scores": {"1. IAM": 82.3, "2. Attack Surface": 60.0},
+ "critical_requirements": [
+ {
+ "requirement_id": "req_new",
+ "title": "New high-risk requirement",
+ "section": "1. IAM",
+ "subsection": "Root Account",
+ "risk_level": 5,
+ "weight": 150,
+ "passed_findings": 7,
+ "total_findings": 10,
+ "description": "Critical requirement description",
+ }
+ ],
+ "total_requirements": 140,
+ "passed_requirements": 100,
+ "failed_requirements": 40,
+ "manual_requirements": 0,
+ "total_findings": 200,
+ "passed_findings": 150,
+ "failed_findings": 50,
+ }
+ mock_compute_metrics.return_value = metrics
+
+ result = generate_threatscore_report_job(
+ tenant_id=str(tenant.id),
+ scan_id=str(scan_current.id),
+ provider_id=str(provider.id),
+ )
+
+ assert result == {"upload": True}
+ mock_compute_metrics.assert_called_once_with(
+ tenant_id=str(tenant.id),
+ scan_id=str(scan_current.id),
+ provider_id=str(provider.id),
+ compliance_id="prowler_threatscore_aws",
+ min_risk_level=4,
+ )
+ mock_generate_report.assert_called_once()
+ mock_upload.assert_called_once()
+ mock_rmtree.assert_called_once()
+
+ snapshots = ThreatScoreSnapshot.objects.filter(
+ tenant=tenant, provider=provider
+ ).order_by("inserted_at")
+ assert snapshots.count() == 2
+
+ new_snapshot = ThreatScoreSnapshot.objects.get(scan=scan_current)
+ assert new_snapshot.compliance_id == "prowler_threatscore_aws"
+ assert Decimal(new_snapshot.overall_score) == Decimal("85.50")
+ assert Decimal(new_snapshot.score_delta) == Decimal("15.50")
+ assert new_snapshot.section_scores == metrics["section_scores"]
+ assert new_snapshot.critical_requirements == metrics["critical_requirements"]
+ assert new_snapshot.total_requirements == metrics["total_requirements"]
+ assert new_snapshot.total_findings == metrics["total_findings"]
@pytest.mark.django_db
From 203b46196b71fe053d0f35ab7db720c45b37f766 Mon Sep 17 00:00:00 2001
From: StylusFrost <43682773+StylusFrost@users.noreply.github.com>
Date: Tue, 11 Nov 2025 15:11:56 +0100
Subject: [PATCH 11/23] fix(test-ui): update authentication method selection in
ProvidersPage for AWS Add Provider e2e test (#9161)
---
ui/playwright.config.ts | 4 +++-
ui/tests/providers/providers-page.ts | 19 +++++++++++++++++++
ui/tests/providers/providers.spec.ts | 5 +++++
3 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts
index 78f840e125..976cb159c1 100644
--- a/ui/playwright.config.ts
+++ b/ui/playwright.config.ts
@@ -13,7 +13,9 @@ export default defineConfig({
},
use: {
- baseURL: "http://localhost:3000",
+ baseURL: process.env.AUTH_URL
+ ? process.env.AUTH_URL
+ : "http://localhost:3000",
trace: "off",
screenshot: "off",
video: "off",
diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts
index 2b638f68b5..abc75d3456 100644
--- a/ui/tests/providers/providers-page.ts
+++ b/ui/tests/providers/providers-page.ts
@@ -689,4 +689,23 @@ export class ProvidersPage extends BasePage {
await this.goto();
await expect(this.providersTable).toBeVisible({ timeout: 10000 });
}
+
+ async selectAuthenticationMethod(method: AWSCredentialType): Promise {
+ // Select the authentication method
+
+ // Search botton that contains text AWS SDK Default or Prowler Cloud will assume or Access & Secret Key
+ const button = this.page.locator('button').filter({ hasText: /AWS SDK Default|Prowler Cloud will assume|Access & Secret Key/i });
+ await button.click();
+
+ if (method === AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN) {
+
+ const modal = this.page.locator('[role="dialog"], .modal, [data-testid*="modal"]').first();
+ await expect(modal).toBeVisible({ timeout: 10000 });
+
+ // Select the role credentials
+ this.page.getByRole('option', { name: 'Access & Secret Key' }).click({ force: true });
+ } else {
+ throw new Error(`Invalid authentication method: ${method}`);
+ }
+ }
}
diff --git a/ui/tests/providers/providers.spec.ts b/ui/tests/providers/providers.spec.ts
index 6471a17863..bc07cdfda6 100644
--- a/ui/tests/providers/providers.spec.ts
+++ b/ui/tests/providers/providers.spec.ts
@@ -164,6 +164,11 @@ test.describe("Add Provider", () => {
);
await providersPage.verifyCredentialsPageLoaded();
+ // Select Authentication Method
+ await providersPage.selectAuthenticationMethod(
+ AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN,
+ );
+
// Fill role credentials
await providersPage.fillRoleCredentials(roleCredentials);
await providersPage.clickNext();
From 136366f4d7211bbc4ae9242cac4e794acab813c2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?=
Date: Tue, 11 Nov 2025 15:34:54 +0100
Subject: [PATCH 12/23] chore(github): enhance metadata for `organization`
service (#9094)
Co-authored-by: Sergio Garcia
---
prowler/CHANGELOG.md | 2 +-
...repository_permission_strict.metadata.json | 25 +++++++++++--------
...ization_members_mfa_required.metadata.json | 22 ++++++++++------
3 files changed, 29 insertions(+), 20 deletions(-)
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index 5ab695d1f3..ad84bb7da5 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -30,6 +30,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS EKS service metadata to new format [(#8890)](https://github.com/prowler-cloud/prowler/pull/8890)
- Update AWS Elastic Beanstalk service metadata to new format [(#8934)](https://github.com/prowler-cloud/prowler/pull/8934)
- Update AWS ElastiCache service metadata to new format [(#8933)](https://github.com/prowler-cloud/prowler/pull/8933)
+- Update GitHub Organization service metadata to new format [(#9094)](https://github.com/prowler-cloud/prowler/pull/9094)
- Update AWS CodeBuild service metadata to new format [(#8851)](https://github.com/prowler-cloud/prowler/pull/8851)
- Update GCP Artifact Registry service metadata to new format [(#9088)](https://github.com/prowler-cloud/prowler/pull/9088)
- Update AWS EFS service metadata to new format [(#8889)](https://github.com/prowler-cloud/prowler/pull/8889)
@@ -39,7 +40,6 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS FSx service metadata to new format [(#9006)](https://github.com/prowler-cloud/prowler/pull/9006)
- Update AWS Glacier service metadata to new format [(#9007)](https://github.com/prowler-cloud/prowler/pull/9007)
- Update oraclecloud analytics service metadata to new format [(#9114)](https://github.com/prowler-cloud/prowler/pull/9114)
-
- Update AWS CodeArtifact service metadata to new format [(#8850)](https://github.com/prowler-cloud/prowler/pull/8850)
- Rename OCI provider to oraclecloud with oci alias [(#9126)](https://github.com/prowler-cloud/prowler/pull/9126)
- Remove unnecessary tests for M365_PowerShell module [(#9204)](https://github.com/prowler-cloud/prowler/pull/9204)
diff --git a/prowler/providers/github/services/organization/organization_default_repository_permission_strict/organization_default_repository_permission_strict.metadata.json b/prowler/providers/github/services/organization/organization_default_repository_permission_strict/organization_default_repository_permission_strict.metadata.json
index 7c2e738958..ccda94f087 100644
--- a/prowler/providers/github/services/organization/organization_default_repository_permission_strict/organization_default_repository_permission_strict.metadata.json
+++ b/prowler/providers/github/services/organization/organization_default_repository_permission_strict/organization_default_repository_permission_strict.metadata.json
@@ -1,32 +1,35 @@
{
"Provider": "github",
"CheckID": "organization_default_repository_permission_strict",
- "CheckTitle": "Ensure strict base repository permissions are set for the organization",
+ "CheckTitle": "Organization base repository permission is read or none",
"CheckType": [],
"ServiceName": "organization",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "GitHubOrganization",
- "Description": "Ensure the organization's base repository permission for members is set to 'read' or 'none' to minimize risk.",
- "Risk": "If base repository permissions allow 'write' or 'admin' by default, organization members may unintentionally gain excessive privileges across repositories, increasing the risk of unauthorized changes or accidental modifications.",
- "RelatedUrl": "https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization",
+ "Description": "**GitHub organization** base repository permission for members uses a **strict setting** such as `read` or `none` rather than permissive options like `write` or `admin`. *Applies to members, not outside collaborators.*",
+ "Risk": "**Excessive default permissions** (`write`/`admin`) erode code **integrity** and **availability**.\n\nAny member-or a compromised account-can alter many repos, inject malicious commits, change tags/releases, or delete branches, enabling supply-chain compromise and large-scale disruptions.",
+ "RelatedUrl": "",
+ "AdditionalURLs": [
+ "https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization"
+ ],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
- "Other": "",
- "Terraform": ""
+ "Other": "1. Sign in to GitHub as an organization owner\n2. Go to the organization > Settings\n3. Under \"Access\" in the sidebar, click \"Member privileges\"\n4. Under \"Base permissions\", select \"Read\" (or \"None\")\n5. Click \"Change default permission\" to confirm",
+ "Terraform": "```hcl\nresource \"github_organization_settings\" \"\" {\n default_repository_permission = \"read\" # Critical: sets the org's base repository permission to a strict level (read/none passes)\n}\n```"
},
"Recommendation": {
- "Text": "Set the organization's base repository permission to 'read' or 'none' for members, unless stricter requirements are needed.",
- "Url": "https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization"
+ "Text": "Apply **least privilege**: set base permission to `none` or `read`.\n\nGrant higher access explicitly via teams per repo and enforce **separation of duties** with required reviews and **branch protection**. Regularly audit memberships and access to limit blast radius and maintain **defense in depth**.",
+ "Url": "https://hub.prowler.com/check/organization_default_repository_permission_strict"
}
},
- "AdditionalURLs": [],
- "Categories": [],
+ "Categories": [
+ "identity-access"
+ ],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
-
diff --git a/prowler/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required.metadata.json b/prowler/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required.metadata.json
index cb1d67e96e..51f0f5518c 100644
--- a/prowler/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required.metadata.json
+++ b/prowler/providers/github/services/organization/organization_members_mfa_required/organization_members_mfa_required.metadata.json
@@ -1,29 +1,35 @@
{
"Provider": "github",
"CheckID": "organization_members_mfa_required",
- "CheckTitle": "Check if organization members are required to have MFA enabled.",
+ "CheckTitle": "Organization requires members to have two-factor authentication enabled",
"CheckType": [],
"ServiceName": "organization",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "GitHubOrganization",
- "Description": "Ensure that all organization members are required to have multi-factor authentication (MFA) enabled. Enforcing MFA for all organization members helps protect the organization's resources and data from unauthorized access and security breaches.",
- "Risk": "Without Multi-Factor Authentication (MFA), user accounts are vulnerable to unauthorized access if their passwords are compromised. This can lead to unauthorized actions such as data theft, malicious code commits, and repository manipulation, potentially compromising the organization's source code and intellectual property.",
- "RelatedUrl": "https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-two-factor-authentication-for-your-organization/requiring-two-factor-authentication-in-your-organization",
+ "Description": "GitHub organization settings require all members to use **two-factor authentication** (2FA).\n\nThe evaluation determines whether access to organization resources is conditioned on members having 2FA enabled.",
+ "Risk": "Without enforced **2FA**, stolen or reused passwords enable account takeover, leading to:\n- Loss of code integrity via unauthorized commits\n- Confidential data exposure from repos and secrets\n- Availability impact from settings changes, token revocation, or deletions",
+ "RelatedUrl": "",
+ "AdditionalURLs": [
+ "https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-two-factor-authentication-for-your-organization/preparing-to-require-two-factor-authentication-in-your-organization",
+ "https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-two-factor-authentication-for-your-organization/requiring-two-factor-authentication-in-your-organization"
+ ],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
- "Other": "",
+ "Other": "1. Sign in to GitHub as an organization owner with 2FA enabled\n2. Go to your organization > Settings\n3. In the left sidebar, click Security > Authentication security\n4. Under Two-factor authentication, select Require two-factor authentication for everyone in your organization\n5. Click Save, then Confirm",
"Terraform": ""
},
"Recommendation": {
- "Text": "Mandate the use of MFA for all organization members. This significantly enhances account security by adding an additional layer of protection beyond a username and password. MFA ensures that even if a password is compromised, unauthorized access to user accounts and repositories is prevented, safeguarding sensitive data and critical assets.",
- "Url": "https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-two-factor-authentication-for-your-organization/preparing-to-require-two-factor-authentication-in-your-organization"
+ "Text": "Enforce org-wide **2FA** for all members and collaborators, preferring **secure methods** (passkeys, security keys, authenticator apps, GitHub Mobile) over SMS.\n\nApply **least privilege**, integrate with **SSO**, restrict token scopes, and use **branch protection** for defense-in-depth. Include bots/service accounts and define recovery options.",
+ "Url": "https://hub.prowler.com/check/organization_members_mfa_required"
}
},
- "Categories": [],
+ "Categories": [
+ "identity-access"
+ ],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
From 1292abcf915ac75e0ea0f8b7e2612be79ee9e46e Mon Sep 17 00:00:00 2001
From: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
Date: Tue, 11 Nov 2025 15:35:45 +0100
Subject: [PATCH 13/23] fix(m365_powershell): restore `MSAL.PS` (#9210)
---
prowler/providers/m365/lib/powershell/m365_powershell.py | 1 +
tests/providers/m365/lib/powershell/m365_powershell_test.py | 5 +++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py
index 48e7a5abbd..6abda172f9 100644
--- a/prowler/providers/m365/lib/powershell/m365_powershell.py
+++ b/prowler/providers/m365/lib/powershell/m365_powershell.py
@@ -853,6 +853,7 @@ def initialize_m365_powershell_modules():
REQUIRED_MODULES = [
"ExchangeOnlineManagement",
"MicrosoftTeams",
+ "MSAL.PS",
]
pwsh = PowerShellSession()
diff --git a/tests/providers/m365/lib/powershell/m365_powershell_test.py b/tests/providers/m365/lib/powershell/m365_powershell_test.py
index b6ba890c2c..84ae2ab129 100644
--- a/tests/providers/m365/lib/powershell/m365_powershell_test.py
+++ b/tests/providers/m365/lib/powershell/m365_powershell_test.py
@@ -312,7 +312,7 @@ class Testm365PowerShell:
assert result is True
# Verify that execute was called for each module
assert (
- mock_execute_obj.call_count == 2 * 3
+ mock_execute_obj.call_count == 3 * 3
) # number of modules * 3 commands each
# Verify success messages were logged
mock_info.assert_any_call(
@@ -381,11 +381,12 @@ class Testm365PowerShell:
main()
# Verify all info messages were logged in the correct order
- assert mock_info.call_count == 3
+ assert mock_info.call_count == 4
mock_info.assert_has_calls(
[
call("Successfully installed module ExchangeOnlineManagement"),
call("Successfully installed module MicrosoftTeams"),
+ call("Successfully installed module MSAL.PS"),
call("M365 PowerShell modules initialized successfully"),
]
)
From b0ec7daece25068aaa445d33393b0e7d882fe792 Mon Sep 17 00:00:00 2001
From: lydiavilchez <114735608+lydiavilchez@users.noreply.github.com>
Date: Tue, 11 Nov 2025 15:51:57 +0100
Subject: [PATCH 14/23] feat(gcp): add check
cloudstorage_bucket_sufficient_retention_period (#9149)
---
prowler/CHANGELOG.md | 1 +
prowler/config/config.yaml | 3 +
...et_log_retention_policy_lock.metadata.json | 21 +-
...torage_bucket_log_retention_policy_lock.py | 13 +-
.../__init__.py | 0
..._sufficient_retention_period.metadata.json | 35 +++
...rage_bucket_sufficient_retention_period.py | 55 +++++
.../cloudstorage/cloudstorage_service.py | 25 ++-
tests/providers/gcp/gcp_provider_test.py | 1 +
...e_bucket_log_retention_policy_lock_test.py | 16 +-
...bucket_sufficient_retention_period_test.py | 202 ++++++++++++++++++
.../cloudstorage/cloudstorage_service_test.py | 15 +-
12 files changed, 367 insertions(+), 20 deletions(-)
create mode 100644 prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/__init__.py
create mode 100644 prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.metadata.json
create mode 100644 prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.py
create mode 100644 tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period_test.py
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index ad84bb7da5..8fcd6aa2dd 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -11,6 +11,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `cloudstorage_bucket_versioning_enabled` check for GCP provider [(#9014)](https://github.com/prowler-cloud/prowler/pull/9014)
- `cloudstorage_bucket_soft_delete_enabled` check for GCP provider [(#9028)](https://github.com/prowler-cloud/prowler/pull/9028)
- `cloudstorage_bucket_logging_enabled` check for GCP provider [(#9091)](https://github.com/prowler-cloud/prowler/pull/9091)
+- `cloudstorage_bucket_sufficient_retention_period` check for GCP provider [(#9149)](https://github.com/prowler-cloud/prowler/pull/9149)
- C5 compliance framework for Azure provider [(#9081)](https://github.com/prowler-cloud/prowler/pull/9081)
- C5 compliance framework for the GCP provider [(#9097)](https://github.com/prowler-cloud/prowler/pull/9097)
- `organization_repository_creation_limited` check for GitHub provider [(#8844)](https://github.com/prowler-cloud/prowler/pull/8844)
diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml
index c3c47b50a9..b9c9a5686c 100644
--- a/prowler/config/config.yaml
+++ b/prowler/config/config.yaml
@@ -511,6 +511,9 @@ gcp:
# gcp.iam_service_account_unused
# gcp.iam_sa_user_managed_key_unused
max_unused_account_days: 180
+ # GCP Storage Sufficient Retention Period
+ # gcp.cloudstorage_bucket_sufficient_retention_period
+ storage_min_retention_days: 90
# Kubernetes Configuration
kubernetes:
diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.metadata.json
index 7064a79d73..9deb2b0fef 100644
--- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.metadata.json
+++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.metadata.json
@@ -1,26 +1,29 @@
{
"Provider": "gcp",
"CheckID": "cloudstorage_bucket_log_retention_policy_lock",
- "CheckTitle": "Ensure That Retention Policies on Cloud Storage Buckets Used for Exporting Logs Are Configured Using Bucket Lock",
+ "CheckTitle": "Cloud Storage log bucket has a Retention Policy with Bucket Lock enabled",
"CheckType": [],
"ServiceName": "cloudstorage",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
- "ResourceType": "Bucket",
- "Description": "Enabling retention policies on log buckets will protect logs stored in cloud storage buckets from being overwritten or accidentally deleted.",
- "Risk": "Sinks can be configured to export logs in storage buckets. It is recommended to configure a data retention policy for these cloud storage buckets and to lock the data retention policy, thus permanently preventing the policy from being reduced or removed. This way, if the system is ever compromised by an attacker or a malicious insider who wants to cover their tracks, the activity logs are definitely preserved for forensics and security investigations.",
+ "ResourceType": "storage.googleapis.com/Bucket",
+ "Description": "**Google Cloud Storage buckets** used as **log sinks** are evaluated to ensure that a **Retention Policy** is configured and **Bucket Lock** is enabled. Enabling Bucket Lock permanently prevents the retention policy from being reduced or removed, protecting logs from modification or deletion.",
+ "Risk": "Log sink buckets without a locked retention policy are at risk of log tampering or accidental deletion. Without Bucket Lock, an attacker or user could remove or shorten the retention policy, compromising the integrity of audit logs required for forensics and compliance investigations.",
"RelatedUrl": "",
+ "AdditionalURLs": [
+ "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/retention-policies-with-bucket-lock.html"
+ ],
"Remediation": {
"Code": {
- "CLI": "",
+ "CLI": "gcloud storage buckets lock-retention-policy gs://",
"NativeIaC": "",
- "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/retention-policies-with-bucket-lock.html",
- "Terraform": "https://docs.prowler.com/checks/gcp/logging-policies-1/ensure-that-retention-policies-on-log-buckets-are-configured-using-bucket-lock#terraform"
+ "Other": "1) Open Google Cloud Console → Storage → Buckets → \n2) Go to the **Configuration** tab\n3) Under **Retention policy**, ensure a retention duration is set\n4) Click **Lock** to enable Bucket Lock and confirm the operation",
+ "Terraform": "```hcl\nresource \"google_storage_bucket\" \"log_bucket\" {\n name = var.log_bucket_name\n location = var.location\n\n retention_policy {\n retention_period = 31536000 # 365 days in seconds\n is_locked = true\n }\n}\n```"
},
"Recommendation": {
- "Text": "It is recommended to set up retention policies and configure Bucket Lock on all storage buckets that are used as log sinks.",
- "Url": "https://cloud.google.com/storage/docs/using-uniform-bucket-level-access"
+ "Text": "Configure a retention policy and enable Bucket Lock on all Cloud Storage buckets used as log sinks to ensure log integrity and immutability.",
+ "Url": "https://hub.prowler.com/check/cloudstorage_bucket_log_retention_policy_lock"
}
},
"Categories": [],
diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.py b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.py
index 180dc6462f..8b186a9732 100644
--- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.py
+++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock.py
@@ -6,7 +6,14 @@ from prowler.providers.gcp.services.logging.logging_client import logging_client
class cloudstorage_bucket_log_retention_policy_lock(Check):
- def execute(self) -> Check_Report_GCP:
+ """
+ Ensure Log Sink buckets have a Retention Policy with Bucket Lock enabled.
+
+ - PASS: Log sink bucket has a retention policy and is locked.
+ - FAIL: Log sink bucket has no retention policy, or it has one but is not locked.
+ """
+
+ def execute(self) -> list[Check_Report_GCP]:
findings = []
# Get Log Sink Buckets
log_buckets = []
@@ -22,8 +29,8 @@ class cloudstorage_bucket_log_retention_policy_lock(Check):
)
if bucket.retention_policy:
report.status = "FAIL"
- report.status_extended = f"Log Sink Bucket {bucket.name} has no Retention Policy but without Bucket Lock."
- if bucket.retention_policy.get("isLocked", False):
+ report.status_extended = f"Log Sink Bucket {bucket.name} has a Retention Policy but without Bucket Lock."
+ if bucket.retention_policy.is_locked:
report.status = "PASS"
report.status_extended = f"Log Sink Bucket {bucket.name} has a Retention Policy with Bucket Lock."
findings.append(report)
diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/__init__.py b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.metadata.json b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.metadata.json
new file mode 100644
index 0000000000..37a627b82c
--- /dev/null
+++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.metadata.json
@@ -0,0 +1,35 @@
+{
+ "Provider": "gcp",
+ "CheckID": "cloudstorage_bucket_sufficient_retention_period",
+ "CheckTitle": "Cloud Storage bucket has a sufficient Retention Policy period",
+ "CheckType": [],
+ "ServiceName": "cloudstorage",
+ "SubServiceName": "",
+ "ResourceIdTemplate": "",
+ "Severity": "medium",
+ "ResourceType": "storage.googleapis.com/Bucket",
+ "Description": "Cloud Storage bucket has a bucket-level Retention Policy with a retentionPeriod that meets or exceeds the organization-defined minimum, preventing deletion or modification of objects before the required time.",
+ "Risk": "Insufficient or missing retention allows premature deletion or modification of objects, weakening data recovery and compliance with retention requirements.",
+ "RelatedUrl": "",
+ "AdditionalURLs": [
+ "https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudStorage/sufficient-retention-period.html"
+ ],
+ "Remediation": {
+ "Code": {
+ "CLI": "gcloud storage buckets update gs:// --retention-period=",
+ "NativeIaC": "",
+ "Other": "1) Console → Storage → Buckets → \n2) Tab 'Configuration' → 'Retention policy'\n3) Set the required retention period (e.g., 90 or 365 days) and save\n4) (Optional) Lock the policy if required by compliance",
+ "Terraform": "```hcl\nresource \"google_storage_bucket\" \"example\" {\n name = var.bucket_name\n location = var.location\n\n retention_policy {\n retention_period = 7776000 # 90 days in seconds\n }\n}\n```"
+ },
+ "Recommendation": {
+ "Text": "Define and apply a bucket-level Retention Policy that meets your minimum retention requirement (e.g., 90 or 365 days) to enforce data recoverability and compliance.",
+ "Url": "https://hub.prowler.com/check/cloudstorage_bucket_sufficient_retention_period"
+ }
+ },
+ "Categories": [
+ "resilience"
+ ],
+ "DependsOn": [],
+ "RelatedTo": [],
+ "Notes": ""
+}
diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.py b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.py
new file mode 100644
index 0000000000..ef635b054d
--- /dev/null
+++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period.py
@@ -0,0 +1,55 @@
+from prowler.lib.check.models import Check, Check_Report_GCP
+from prowler.providers.gcp.services.cloudstorage.cloudstorage_client import (
+ cloudstorage_client,
+)
+
+
+class cloudstorage_bucket_sufficient_retention_period(Check):
+ """
+ Ensure there is a sufficient bucket-level retention period configured for GCS buckets.
+
+ PASS: retentionPolicy.retentionPeriod >= min threshold (days)
+ FAIL: no retention policy or period < threshold
+ """
+
+ def execute(self) -> list[Check_Report_GCP]:
+ findings = []
+
+ min_retention_days = int(
+ getattr(cloudstorage_client, "audit_config", {}).get(
+ "storage_min_retention_days", 90
+ )
+ )
+
+ for bucket in cloudstorage_client.buckets:
+ report = Check_Report_GCP(metadata=self.metadata(), resource=bucket)
+
+ retention_policy = bucket.retention_policy
+
+ if retention_policy is None:
+ report.status = "FAIL"
+ report.status_extended = (
+ f"Bucket {bucket.name} does not have a retention policy "
+ f"(minimum required: {min_retention_days} days)."
+ )
+ findings.append(report)
+ continue
+
+ days = retention_policy.retention_period // 86400 # seconds to days
+
+ if days >= min_retention_days:
+ report.status = "PASS"
+ report.status_extended = (
+ f"Bucket {bucket.name} has a sufficient retention policy of {days} days "
+ f"(minimum required: {min_retention_days})."
+ )
+ else:
+ report.status = "FAIL"
+ report.status_extended = (
+ f"Bucket {bucket.name} has an insufficient retention policy of {days} days "
+ f"(minimum required: {min_retention_days})."
+ )
+
+ findings.append(report)
+
+ return findings
diff --git a/prowler/providers/gcp/services/cloudstorage/cloudstorage_service.py b/prowler/providers/gcp/services/cloudstorage/cloudstorage_service.py
index 23c44dcc8c..cdfdd5c18c 100644
--- a/prowler/providers/gcp/services/cloudstorage/cloudstorage_service.py
+++ b/prowler/providers/gcp/services/cloudstorage/cloudstorage_service.py
@@ -56,6 +56,21 @@ class CloudStorage(GCPService):
logging_bucket = logging_info.get("logBucket")
logging_prefix = logging_info.get("logObjectPrefix")
+ retention_policy_raw = bucket.get("retentionPolicy")
+ retention_policy = None
+ if isinstance(retention_policy_raw, dict):
+ rp_seconds = retention_policy_raw.get("retentionPeriod")
+ if rp_seconds:
+ retention_policy = RetentionPolicy(
+ retention_period=int(rp_seconds),
+ is_locked=bool(
+ retention_policy_raw.get("isLocked", False)
+ ),
+ effective_time=retention_policy_raw.get(
+ "effectiveTime"
+ ),
+ )
+
self.buckets.append(
Bucket(
name=bucket["name"],
@@ -65,7 +80,7 @@ class CloudStorage(GCPService):
"uniformBucketLevelAccess"
]["enabled"],
public=public,
- retention_policy=bucket.get("retentionPolicy"),
+ retention_policy=retention_policy,
project_id=project_id,
lifecycle_rules=lifecycle_rules,
versioning_enabled=versioning_enabled,
@@ -84,6 +99,12 @@ class CloudStorage(GCPService):
)
+class RetentionPolicy(BaseModel):
+ retention_period: int
+ is_locked: bool
+ effective_time: Optional[str] = None
+
+
class Bucket(BaseModel):
name: str
id: str
@@ -91,7 +112,7 @@ class Bucket(BaseModel):
uniform_bucket_level_access: bool
public: bool
project_id: str
- retention_policy: Optional[dict] = None
+ retention_policy: Optional[RetentionPolicy] = None
lifecycle_rules: Optional[list[dict]] = None
versioning_enabled: Optional[bool] = False
soft_delete_enabled: Optional[bool] = False
diff --git a/tests/providers/gcp/gcp_provider_test.py b/tests/providers/gcp/gcp_provider_test.py
index 63741ed647..ad01635a99 100644
--- a/tests/providers/gcp/gcp_provider_test.py
+++ b/tests/providers/gcp/gcp_provider_test.py
@@ -90,6 +90,7 @@ class TestGCPProvider:
assert gcp_provider.audit_config == {
"shodan_api_key": None,
"max_unused_account_days": 180,
+ "storage_min_retention_days": 90,
}
@freeze_time(datetime.today())
diff --git a/tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock_test.py b/tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock_test.py
index f52adeb553..c2c67be39a 100644
--- a/tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock_test.py
+++ b/tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_log_retention_policy_lock/cloudstorage_bucket_log_retention_policy_lock_test.py
@@ -31,6 +31,7 @@ class TestCloudStorageBucketLogRetentionPolicyLock:
)
from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import (
Bucket,
+ RetentionPolicy,
)
from prowler.providers.gcp.services.logging.logging_service import Sink
@@ -53,7 +54,11 @@ class TestCloudStorageBucketLogRetentionPolicyLock:
region=GCP_US_CENTER1_LOCATION,
uniform_bucket_level_access=True,
public=True,
- retention_policy={"isLocked": True},
+ retention_policy=RetentionPolicy(
+ retention_period=31536000,
+ is_locked=True,
+ effective_time=None,
+ ),
project_id=GCP_PROJECT_ID,
)
]
@@ -95,6 +100,7 @@ class TestCloudStorageBucketLogRetentionPolicyLock:
)
from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import (
Bucket,
+ RetentionPolicy,
)
from prowler.providers.gcp.services.logging.logging_service import Sink
@@ -117,7 +123,11 @@ class TestCloudStorageBucketLogRetentionPolicyLock:
region=GCP_US_CENTER1_LOCATION,
uniform_bucket_level_access=True,
public=True,
- retention_policy={"isLocked": False},
+ retention_policy=RetentionPolicy(
+ retention_period=31536000,
+ is_locked=False,
+ effective_time=None,
+ ),
project_id=GCP_PROJECT_ID,
)
]
@@ -129,7 +139,7 @@ class TestCloudStorageBucketLogRetentionPolicyLock:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
- == f"Log Sink Bucket {cloudstorage_client.buckets[0].name} has no Retention Policy but without Bucket Lock."
+ == f"Log Sink Bucket {cloudstorage_client.buckets[0].name} has a Retention Policy but without Bucket Lock."
)
assert result[0].resource_id == "example-bucket"
assert result[0].resource_name == "example-bucket"
diff --git a/tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period_test.py b/tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period_test.py
new file mode 100644
index 0000000000..706c9f2062
--- /dev/null
+++ b/tests/providers/gcp/services/cloudstorage/cloudstorage_bucket_sufficient_retention_period/cloudstorage_bucket_sufficient_retention_period_test.py
@@ -0,0 +1,202 @@
+from unittest import mock
+
+from tests.providers.gcp.gcp_fixtures import (
+ GCP_PROJECT_ID,
+ GCP_US_CENTER1_LOCATION,
+ set_mocked_gcp_provider,
+)
+
+
+class TestCloudStorageBucketSufficientRetentionPeriod:
+ def test_no_buckets(self):
+ cloudstorage_client = mock.MagicMock()
+
+ with (
+ mock.patch(
+ "prowler.providers.common.provider.Provider.get_global_provider",
+ return_value=set_mocked_gcp_provider(),
+ ),
+ mock.patch(
+ "prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_sufficient_retention_period.cloudstorage_bucket_sufficient_retention_period.cloudstorage_client",
+ new=cloudstorage_client,
+ ),
+ ):
+ from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_sufficient_retention_period.cloudstorage_bucket_sufficient_retention_period import (
+ cloudstorage_bucket_sufficient_retention_period,
+ )
+
+ cloudstorage_client.project_ids = [GCP_PROJECT_ID]
+ cloudstorage_client.region = GCP_US_CENTER1_LOCATION
+ cloudstorage_client.buckets = []
+ cloudstorage_client.audit_config = {"storage_min_retention_days": 90}
+
+ check = cloudstorage_bucket_sufficient_retention_period()
+ result = check.execute()
+
+ assert len(result) == 0
+
+ def test_bucket_without_retention_policy(self):
+ cloudstorage_client = mock.MagicMock()
+
+ with (
+ mock.patch(
+ "prowler.providers.common.provider.Provider.get_global_provider",
+ return_value=set_mocked_gcp_provider(),
+ ),
+ mock.patch(
+ "prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_sufficient_retention_period.cloudstorage_bucket_sufficient_retention_period.cloudstorage_client",
+ new=cloudstorage_client,
+ ),
+ ):
+ from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_sufficient_retention_period.cloudstorage_bucket_sufficient_retention_period import (
+ cloudstorage_bucket_sufficient_retention_period,
+ )
+ from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import (
+ Bucket,
+ )
+
+ cloudstorage_client.project_ids = [GCP_PROJECT_ID]
+ cloudstorage_client.region = GCP_US_CENTER1_LOCATION
+ cloudstorage_client.audit_config = {"storage_min_retention_days": 90}
+
+ cloudstorage_client.buckets = [
+ Bucket(
+ name="no-retention-policy",
+ id="no-retention-policy",
+ region=GCP_US_CENTER1_LOCATION,
+ uniform_bucket_level_access=True,
+ public=False,
+ retention_policy=None,
+ project_id=GCP_PROJECT_ID,
+ lifecycle_rules=[],
+ versioning_enabled=True,
+ )
+ ]
+
+ check = cloudstorage_bucket_sufficient_retention_period()
+ result = check.execute()
+
+ assert len(result) == 1
+ assert result[0].status == "FAIL"
+ assert (
+ result[0].status_extended
+ == "Bucket no-retention-policy does not have a retention policy (minimum required: 90 days)."
+ )
+ assert result[0].resource_id == "no-retention-policy"
+ assert result[0].resource_name == "no-retention-policy"
+ assert result[0].location == GCP_US_CENTER1_LOCATION
+ assert result[0].project_id == GCP_PROJECT_ID
+
+ def test_bucket_with_sufficient_retention_policy(self):
+ cloudstorage_client = mock.MagicMock()
+
+ with (
+ mock.patch(
+ "prowler.providers.common.provider.Provider.get_global_provider",
+ return_value=set_mocked_gcp_provider(),
+ ),
+ mock.patch(
+ "prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_sufficient_retention_period.cloudstorage_bucket_sufficient_retention_period.cloudstorage_client",
+ new=cloudstorage_client,
+ ),
+ ):
+ from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_sufficient_retention_period.cloudstorage_bucket_sufficient_retention_period import (
+ cloudstorage_bucket_sufficient_retention_period,
+ )
+ from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import (
+ Bucket,
+ RetentionPolicy,
+ )
+
+ cloudstorage_client.project_ids = [GCP_PROJECT_ID]
+ cloudstorage_client.region = GCP_US_CENTER1_LOCATION
+ cloudstorage_client.audit_config = {"storage_min_retention_days": 90}
+
+ cloudstorage_client.buckets = [
+ Bucket(
+ name="sufficient-retention-policy",
+ id="sufficient-retention-policy",
+ region=GCP_US_CENTER1_LOCATION,
+ uniform_bucket_level_access=True,
+ public=False,
+ retention_policy=RetentionPolicy(
+ retention_period=12096000, # 140 days
+ is_locked=False,
+ effective_time=None,
+ ),
+ project_id=GCP_PROJECT_ID,
+ lifecycle_rules=[],
+ versioning_enabled=True,
+ )
+ ]
+
+ check = cloudstorage_bucket_sufficient_retention_period()
+ result = check.execute()
+
+ assert len(result) == 1
+ assert result[0].status == "PASS"
+ assert (
+ result[0].status_extended
+ == "Bucket sufficient-retention-policy has a sufficient retention policy of 140 days (minimum required: 90)."
+ )
+ assert result[0].resource_id == "sufficient-retention-policy"
+ assert result[0].resource_name == "sufficient-retention-policy"
+ assert result[0].location == GCP_US_CENTER1_LOCATION
+ assert result[0].project_id == GCP_PROJECT_ID
+
+ def test_bucket_with_insufficient_retention_policy(self):
+ cloudstorage_client = mock.MagicMock()
+
+ with (
+ mock.patch(
+ "prowler.providers.common.provider.Provider.get_global_provider",
+ return_value=set_mocked_gcp_provider(),
+ ),
+ mock.patch(
+ "prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_sufficient_retention_period.cloudstorage_bucket_sufficient_retention_period.cloudstorage_client",
+ new=cloudstorage_client,
+ ),
+ ):
+ from prowler.providers.gcp.services.cloudstorage.cloudstorage_bucket_sufficient_retention_period.cloudstorage_bucket_sufficient_retention_period import (
+ cloudstorage_bucket_sufficient_retention_period,
+ )
+ from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import (
+ Bucket,
+ RetentionPolicy,
+ )
+
+ cloudstorage_client.project_ids = [GCP_PROJECT_ID]
+ cloudstorage_client.region = GCP_US_CENTER1_LOCATION
+ cloudstorage_client.audit_config = {"storage_min_retention_days": 90}
+
+ cloudstorage_client.buckets = [
+ Bucket(
+ name="insufficient-retention-policy",
+ id="insufficient-retention-policy",
+ region=GCP_US_CENTER1_LOCATION,
+ uniform_bucket_level_access=True,
+ public=False,
+ retention_policy=RetentionPolicy(
+ retention_period=604800, # 7 days
+ is_locked=False,
+ effective_time=None,
+ ),
+ project_id=GCP_PROJECT_ID,
+ lifecycle_rules=[],
+ versioning_enabled=True,
+ )
+ ]
+
+ check = cloudstorage_bucket_sufficient_retention_period()
+ result = check.execute()
+
+ assert len(result) == 1
+ assert result[0].status == "FAIL"
+ assert (
+ result[0].status_extended
+ == "Bucket insufficient-retention-policy has an insufficient retention policy of 7 days (minimum required: 90)."
+ )
+ assert result[0].resource_id == "insufficient-retention-policy"
+ assert result[0].resource_name == "insufficient-retention-policy"
+ assert result[0].location == GCP_US_CENTER1_LOCATION
+ assert result[0].project_id == GCP_PROJECT_ID
diff --git a/tests/providers/gcp/services/cloudstorage/cloudstorage_service_test.py b/tests/providers/gcp/services/cloudstorage/cloudstorage_service_test.py
index fcd8761db5..0d98e00f6e 100644
--- a/tests/providers/gcp/services/cloudstorage/cloudstorage_service_test.py
+++ b/tests/providers/gcp/services/cloudstorage/cloudstorage_service_test.py
@@ -2,6 +2,7 @@ from unittest.mock import patch
from prowler.providers.gcp.services.cloudstorage.cloudstorage_service import (
CloudStorage,
+ RetentionPolicy,
)
from tests.providers.gcp.gcp_fixtures import (
GCP_PROJECT_ID,
@@ -35,9 +36,17 @@ class TestCloudStorageService:
assert cloudstorage_client.buckets[0].region == "US"
assert cloudstorage_client.buckets[0].uniform_bucket_level_access
assert cloudstorage_client.buckets[0].public
- assert cloudstorage_client.buckets[0].retention_policy == {
- "retentionPeriod": 10
- }
+
+ assert isinstance(
+ cloudstorage_client.buckets[0].retention_policy, RetentionPolicy
+ )
+ assert (
+ cloudstorage_client.buckets[0].retention_policy.retention_period == 10
+ )
+ assert cloudstorage_client.buckets[0].retention_policy.is_locked is False
+ assert (
+ cloudstorage_client.buckets[0].retention_policy.effective_time is None
+ )
assert cloudstorage_client.buckets[0].project_id == GCP_PROJECT_ID
assert cloudstorage_client.buckets[1].name == "bucket2"
From 1ec36d22858e5681bad0b51f3c50212c85b2040e Mon Sep 17 00:00:00 2001
From: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
Date: Tue, 11 Nov 2025 16:11:24 +0100
Subject: [PATCH 15/23] docs: add Prowler Cloud public IPs (#9209)
---
docs/docs.json | 1 +
.../tutorials/prowler-cloud-public-ips.mdx | 29 +++++++++++++++++++
2 files changed, 30 insertions(+)
create mode 100644 docs/user-guide/tutorials/prowler-cloud-public-ips.mdx
diff --git a/docs/docs.json b/docs/docs.json
index 54402e5d8a..cf6d828e42 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -110,6 +110,7 @@
]
},
"user-guide/tutorials/prowler-app-lighthouse",
+ "user-guide/tutorials/prowler-cloud-public-ips",
{
"group": "Tutorials",
"pages": [
diff --git a/docs/user-guide/tutorials/prowler-cloud-public-ips.mdx b/docs/user-guide/tutorials/prowler-cloud-public-ips.mdx
new file mode 100644
index 0000000000..f3b285056c
--- /dev/null
+++ b/docs/user-guide/tutorials/prowler-cloud-public-ips.mdx
@@ -0,0 +1,29 @@
+---
+title: 'Prowler Cloud Public IPs'
+---
+
+## Overview
+
+Prowler Cloud uses a dedicated egress IPv4 address for all outbound connections to customer infrastructure. This enables organizations to implement network-level security controls by whitelisting Prowler's IP address.
+
+## Use Cases
+
+Whitelisting Prowler's egress IP address enables:
+
+- **Credential Usage Control**: Restrict where cloud provider credentials can be used from across AWS, Azure, GCP, and other providers
+- **Kubernetes Security**: Limit inbound HTTPS traffic to clusters by allowing only Prowler's IP address
+- **Compliance Requirements**: Meet security policies requiring allowlisting of external services
+
+## Query the Egress IP Address
+
+Retrieve Prowler Cloud's current egress IP address using the following command:
+
+```bash
+dig egress.prowler.com +short
+```
+
+This command returns the IPv4 address that Prowler Cloud uses for all outbound connections to customer infrastructure.
+
+
+The egress IP address is stable, but it is recommended to periodically verify it remains current by querying `egress.prowler.com`.
+
From 7cb0ed052df59f86f2b16d9d9257f8b06f6444e7 Mon Sep 17 00:00:00 2001
From: Josema Camacho
Date: Tue, 11 Nov 2025 16:51:28 +0100
Subject: [PATCH 16/23] chore(security): upgrading django to 5.1.14 (#9176)
---
api/CHANGELOG.md | 3 +++
api/poetry.lock | 10 +++++-----
api/pyproject.toml | 2 +-
3 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md
index 5df4b97764..71268ebf2f 100644
--- a/api/CHANGELOG.md
+++ b/api/CHANGELOG.md
@@ -17,6 +17,9 @@ All notable changes to the **Prowler API** are documented in this file.
- Tenant-wide ThreatScore overview aggregation and snapshot persistence with backfill support [(#9148)](https://github.com/prowler-cloud/prowler/pull/9148)
- Support for MongoDB Atlas provider [(#9167)](https://github.com/prowler-cloud/prowler/pull/9167)
+### Security
+- Django updated to the latest 5.1 security release, 5.1.14, due to problems with potential [SQL injection](https://github.com/prowler-cloud/prowler/security/dependabot/113) and [denial-of-service vulnerability](https://github.com/prowler-cloud/prowler/security/dependabot/114) [(#9176)](https://github.com/prowler-cloud/prowler/pull/9176)
+
---
## [1.14.2] (Prowler 5.13.2)
diff --git a/api/poetry.lock b/api/poetry.lock
index 40313d9fa5..332d4d9f73 100644
--- a/api/poetry.lock
+++ b/api/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -1671,14 +1671,14 @@ with-social = ["django-allauth[socialaccount] (>=64.0.0)"]
[[package]]
name = "django"
-version = "5.1.13"
+version = "5.1.14"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
files = [
- {file = "django-5.1.13-py3-none-any.whl", hash = "sha256:06f257f79dc4c17f3f9e23b106a4c5ed1335abecbe731e83c598c941d14fbeed"},
- {file = "django-5.1.13.tar.gz", hash = "sha256:543ff21679f15e80edfc01fe7ea35f8291b6d4ea589433882913626a7c1cf929"},
+ {file = "django-5.1.14-py3-none-any.whl", hash = "sha256:2a4b9c20404fd1bf50aaaa5542a19d860594cba1354f688f642feb271b91df27"},
+ {file = "django-5.1.14.tar.gz", hash = "sha256:b98409fb31fdd6e8c3a6ba2eef3415cc5c0020057b43b21ba7af6eff5f014831"},
]
[package.dependencies]
@@ -6786,4 +6786,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<3.13"
-content-hash = "3c9164d668d37d6373eb5200bbe768232ead934d9312b9c68046b1df922789f3"
+content-hash = "943e2cd6b87229704550d4e140b36509fb9f58896ebb5834b9fbabe28a9ee92f"
diff --git a/api/pyproject.toml b/api/pyproject.toml
index 22ae14e6a1..ffc8413489 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -7,7 +7,7 @@ authors = [{name = "Prowler Engineering", email = "engineering@prowler.com"}]
dependencies = [
"celery[pytest] (>=5.4.0,<6.0.0)",
"dj-rest-auth[with_social,jwt] (==7.0.1)",
- "django (==5.1.13)",
+ "django (==5.1.14)",
"django-allauth[saml] (>=65.8.0,<66.0.0)",
"django-celery-beat (>=2.7.0,<3.0.0)",
"django-celery-results (>=2.5.1,<3.0.0)",
From 0f22e754f225544891e7db252941361b10ebf4f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?=
Date: Tue, 11 Nov 2025 17:10:40 +0100
Subject: [PATCH 17/23] chore(mongodbatlas): enhance metadata for `projects`
service (#9093)
Co-authored-by: Sergio Garcia
---
prowler/CHANGELOG.md | 1 +
.../projects_auditing_enabled.metadata.json | 29 +++++++++++++------
...ess_list_exposed_to_internet.metadata.json | 23 +++++++++------
3 files changed, 35 insertions(+), 18 deletions(-)
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index 8fcd6aa2dd..9c4e8812af 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -31,6 +31,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update AWS EKS service metadata to new format [(#8890)](https://github.com/prowler-cloud/prowler/pull/8890)
- Update AWS Elastic Beanstalk service metadata to new format [(#8934)](https://github.com/prowler-cloud/prowler/pull/8934)
- Update AWS ElastiCache service metadata to new format [(#8933)](https://github.com/prowler-cloud/prowler/pull/8933)
+- Update MongoDB Atlas projects service metadata to new format [(#9093)](https://github.com/prowler-cloud/prowler/pull/9093)
- Update GitHub Organization service metadata to new format [(#9094)](https://github.com/prowler-cloud/prowler/pull/9094)
- Update AWS CodeBuild service metadata to new format [(#8851)](https://github.com/prowler-cloud/prowler/pull/8851)
- Update GCP Artifact Registry service metadata to new format [(#9088)](https://github.com/prowler-cloud/prowler/pull/9088)
diff --git a/prowler/providers/mongodbatlas/services/projects/projects_auditing_enabled/projects_auditing_enabled.metadata.json b/prowler/providers/mongodbatlas/services/projects/projects_auditing_enabled/projects_auditing_enabled.metadata.json
index 35a58668ce..28d288e055 100644
--- a/prowler/providers/mongodbatlas/services/projects/projects_auditing_enabled/projects_auditing_enabled.metadata.json
+++ b/prowler/providers/mongodbatlas/services/projects/projects_auditing_enabled/projects_auditing_enabled.metadata.json
@@ -1,29 +1,40 @@
{
"Provider": "mongodbatlas",
"CheckID": "projects_auditing_enabled",
- "CheckTitle": "Ensure database auditing is enabled",
+ "CheckTitle": "MongoDB Atlas project has database auditing enabled",
"CheckType": [],
"ServiceName": "projects",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "MongoDBAtlasProject",
- "Description": "Ensure database auditing is enabled to track database operations and security events",
- "Risk": "Without auditing enabled, security events and database operations are not logged, making it difficult to detect unauthorized access or troubleshoot issues",
+ "Description": "**MongoDB Atlas projects** with **database auditing** capture database operations and administrative events. The evaluation looks for an active audit configuration and, *when present*, notes any configured `audit_filter` that scopes which events are recorded.",
+ "Risk": "Without auditing, critical actions lack traceability, reducing **detectability** and impeding **forensics**. Attackers can mask unauthorized reads/writes and privilege changes, threatening data **confidentiality** and **integrity**, and weakening non-repudiation and incident response.",
"RelatedUrl": "",
+ "AdditionalURLs": [
+ "https://www.mongodb.com/docs/manual/tutorial/configure-auditing/",
+ "https://www.mongodb.com/docs/atlas/architecture/current/auditing/",
+ "https://www.mongodb.com/docs/atlas/architecture/current/auditing-logging/?msockid=0878cc3dfa4e66a707beda0efb5a67b5",
+ "https://www.mongodb.com/docs/atlas/operator/current/ak8so-configure-audit-logs/",
+ "https://www.mongodb.com/docs/manual/core/auditing/",
+ "https://www.mongodb.com/docs/atlas/database-auditing/"
+ ],
"Remediation": {
"Code": {
- "CLI": "",
+ "CLI": "atlas auditing update --projectId --enabled",
"NativeIaC": "",
- "Other": "",
- "Terraform": ""
+ "Other": "1. Sign in to MongoDB Atlas and open the target project\n2. In the left sidebar, click Security > Database & Network Access, then click Advanced\n3. Toggle Database Auditing to On\n4. Click Save",
+ "Terraform": "```hcl\nresource \"mongodbatlas_auditing\" \"example\" {\n project_id = \"\"\n enabled = true # Critical: turns on project-level database auditing to pass the check\n}\n```"
},
"Recommendation": {
- "Text": "Enable database auditing for the MongoDB Atlas project by configuring audit filters and destinations.",
- "Url": "https://www.mongodb.com/docs/atlas/database-auditing/"
+ "Text": "Enable **auditing** and apply least-privilege filters to capture high-risk events:\n- authentication and session activity\n- DDL/config changes\n- user/role modifications and privilege grants\n\nCentralize logs in a SIEM, enforce retention/immutability with separation of duties, restrict access, and tune `auditAuthorizationSuccess` to balance coverage vs performance.",
+ "Url": "https://hub.prowler.com/check/projects_auditing_enabled"
}
},
- "Categories": [],
+ "Categories": [
+ "logging",
+ "forensics-ready"
+ ],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
diff --git a/prowler/providers/mongodbatlas/services/projects/projects_network_access_list_exposed_to_internet/projects_network_access_list_exposed_to_internet.metadata.json b/prowler/providers/mongodbatlas/services/projects/projects_network_access_list_exposed_to_internet/projects_network_access_list_exposed_to_internet.metadata.json
index b17d54c5bd..704aec7a00 100644
--- a/prowler/providers/mongodbatlas/services/projects/projects_network_access_list_exposed_to_internet/projects_network_access_list_exposed_to_internet.metadata.json
+++ b/prowler/providers/mongodbatlas/services/projects/projects_network_access_list_exposed_to_internet/projects_network_access_list_exposed_to_internet.metadata.json
@@ -1,29 +1,34 @@
{
"Provider": "mongodbatlas",
"CheckID": "projects_network_access_list_exposed_to_internet",
- "CheckTitle": "Ensure MongoDB Atlas project network access list is not exposed to the internet",
+ "CheckTitle": "MongoDB Atlas project network access list has entries and excludes 0.0.0.0/0, ::/0, 0.0.0.0, and ::",
"CheckType": [],
"ServiceName": "projects",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "MongoDBAtlasProject",
- "Description": "Ensure that MongoDB Atlas projects have properly configured network access lists that don't allow unrestricted access from anywhere on the internet. Network access lists should be configured to allow access only from specific IP addresses, CIDR blocks, or AWS security groups to minimize the attack surface.",
- "Risk": "If a MongoDB Atlas project has network access entries that allow unrestricted access (0.0.0.0/0 or ::/0), it exposes the database to potential attacks from anywhere on the internet. This significantly increases the risk of unauthorized access, data breaches, and malicious activities.",
- "RelatedUrl": "https://docs.atlas.mongodb.com/security/ip-access-list/",
+ "Description": "**MongoDB Atlas project network access list** configuration is evaluated for entries that allow access from anywhere (`0.0.0.0/0`, `::/0`, `0.0.0.0`, `::`) or for missing access lists, instead of restricting connections to specific IPs or CIDRs.",
+ "Risk": "Internet-wide access enables scanning, brute force, and credential stuffing against database endpoints. A successful compromise can cause data exfiltration (**confidentiality**), unauthorized writes or drops (**integrity**), and service disruption or lockout (**availability**).",
+ "RelatedUrl": "",
+ "AdditionalURLs": [
+ "https://docs.atlas.mongodb.com/security/ip-access-list/"
+ ],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
- "Other": "",
- "Terraform": ""
+ "Other": "1. In MongoDB Atlas, open your project and go to Security > Database & Network Access > IP Access List\n2. Delete any entries equal to 0.0.0.0/0, ::/0, 0.0.0.0, or ::\n3. If the list becomes empty, click Add IP Address and add a specific IP/CIDR or an AWS Security Group (for a peered VPC)\n4. Click Save",
+ "Terraform": "```hcl\nresource \"mongodbatlas_project_ip_access_list\" \"\" {\n project_id = \"\"\n cidr_block = \"\" # Critical: add a restricted CIDR (not 0.0.0.0/0 or ::/0) to ensure the list isn't empty and not open to the world\n}\n```"
},
"Recommendation": {
- "Text": "Configure network access lists to allow access only from specific IP addresses, CIDR blocks, or AWS security groups. Remove any entries that allow unrestricted access (0.0.0.0/0 or ::/0) and replace them with more restrictive rules based on your application's requirements.",
- "Url": "https://docs.atlas.mongodb.com/security/ip-access-list/"
+ "Text": "Apply **least privilege**: permit only required IPs/CIDRs or approved security groups; avoid `0.0.0.0/0` and `::/0`. Prefer **private connectivity** (VPC peering or private endpoints) over public access. Use temporary entries for short-lived admin needs and review lists regularly.",
+ "Url": "https://hub.prowler.com/check/projects_network_access_list_exposed_to_internet"
}
},
- "Categories": [],
+ "Categories": [
+ "internet-exposed"
+ ],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
From ccb269caa2260f3f59b0c7a62eff931208cab52e Mon Sep 17 00:00:00 2001
From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Date: Tue, 11 Nov 2025 17:12:42 +0100
Subject: [PATCH 18/23] chore(dependencies): add Sentry to /ui (#8730)
Co-authored-by: Alan Buscaglia
Co-authored-by: Pepe Fagoaga
---
.env | 9 +
ui/actions/auth/auth.ts | 3 +
ui/actions/scans/scans.ts | 13 +-
ui/actions/task/poll.ts | 6 +
ui/app/(prowler)/error.tsx | 34 +-
ui/app/(prowler)/layout.tsx | 4 +
ui/app/api/lighthouse/analyst/route.ts | 39 +-
ui/app/global-error.tsx | 40 +
ui/app/instrumentation.client.ts | 115 +
ui/app/providers.tsx | 3 +
ui/dependency-log.json | 10 +-
ui/instrumentation.ts | 30 +
ui/lib/sentry-breadcrumbs.ts | 179 ++
ui/lib/server-actions-helper.ts | 120 +-
ui/next.config.js | 78 +-
ui/package-lock.json | 3442 +++++++++++++++++++++---
ui/package.json | 1 +
ui/sentry/README.md | 52 +
ui/sentry/index.ts | 2 +
ui/sentry/sentry.edge.config.ts | 64 +
ui/sentry/sentry.server.config.ts | 80 +
ui/sentry/utils.ts | 36 +
22 files changed, 3938 insertions(+), 422 deletions(-)
create mode 100644 ui/app/global-error.tsx
create mode 100644 ui/app/instrumentation.client.ts
create mode 100644 ui/instrumentation.ts
create mode 100644 ui/lib/sentry-breadcrumbs.ts
create mode 100644 ui/sentry/README.md
create mode 100644 ui/sentry/index.ts
create mode 100644 ui/sentry/sentry.edge.config.ts
create mode 100644 ui/sentry/sentry.server.config.ts
create mode 100644 ui/sentry/utils.ts
diff --git a/.env b/.env
index f83f8ea702..d7a4966ff9 100644
--- a/.env
+++ b/.env
@@ -14,6 +14,15 @@ UI_PORT=3000
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
# Google Tag Manager ID
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=""
+# Sentry
+SENTRY_DSN=
+NEXT_PUBLIC_SENTRY_DSN=
+SENTRY_ORG=
+SENTRY_PROJECT=
+SENTRY_AUTH_TOKEN=
+SENTRY_ENVIRONMENT=production
+NEXT_PUBLIC_SENTRY_ENVIRONMENT=production
+
#### Code Review Configuration ####
# Enable Claude Code standards validation on pre-push hook
# Set to 'true' to validate changes against AGENTS.md standards via Claude Code
diff --git a/ui/actions/auth/auth.ts b/ui/actions/auth/auth.ts
index 5555e6d9e9..9dfdde4d9a 100644
--- a/ui/actions/auth/auth.ts
+++ b/ui/actions/auth/auth.ts
@@ -4,6 +4,7 @@ import { AuthError } from "next-auth";
import { signIn, signOut } from "@/auth.config";
import { apiBaseUrl } from "@/lib";
+import { addAuthEvent } from "@/lib/sentry-breadcrumbs";
import type { SignInFormData, SignUpFormData } from "@/types";
export async function authenticate(
@@ -11,6 +12,7 @@ export async function authenticate(
formData: SignInFormData,
) {
try {
+ addAuthEvent("login", { email: formData.email });
await signIn("credentials", {
...formData,
redirect: false,
@@ -20,6 +22,7 @@ export async function authenticate(
};
} catch (error) {
if (error instanceof AuthError) {
+ addAuthEvent("error", { type: error.type });
switch (error.type) {
case "CredentialsSignin":
return {
diff --git a/ui/actions/scans/scans.ts b/ui/actions/scans/scans.ts
index 6dc3654344..1442c1a342 100644
--- a/ui/actions/scans/scans.ts
+++ b/ui/actions/scans/scans.ts
@@ -3,6 +3,7 @@
import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders, getErrorMessage } from "@/lib";
+import { addScanOperation } from "@/lib/sentry-breadcrumbs";
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
export const getScans = async ({
@@ -89,6 +90,11 @@ export const scanOnDemand = async (formData: FormData) => {
return { error: "Provider ID is required" };
}
+ addScanOperation("create", undefined, {
+ provider_id: String(providerId),
+ scan_name: scanName ? String(scanName) : undefined,
+ });
+
const url = new URL(`${apiBaseUrl}/scans`);
try {
@@ -113,8 +119,13 @@ export const scanOnDemand = async (formData: FormData) => {
body: JSON.stringify(requestBody),
});
- return handleApiResponse(response, "/scans");
+ const result = await handleApiResponse(response, "/scans");
+ if (result?.data?.id) {
+ addScanOperation("start", result.data.id);
+ }
+ return result;
} catch (error) {
+ addScanOperation("create");
return handleApiError(error);
}
};
diff --git a/ui/actions/task/poll.ts b/ui/actions/task/poll.ts
index 0dcf34af64..3e8a325a7b 100644
--- a/ui/actions/task/poll.ts
+++ b/ui/actions/task/poll.ts
@@ -1,6 +1,7 @@
"use server";
import { getTask } from "@/actions/task/tasks";
+import { addTaskEvent } from "@/lib/sentry-breadcrumbs";
import type {
GetTaskResponse,
PollOptions,
@@ -14,10 +15,12 @@ export async function pollTaskUntilSettled(
taskId: string,
{ maxAttempts = 10, delayMs = 2000 }: PollOptions = {},
): Promise> {
+ addTaskEvent("started", taskId, { max_attempts: maxAttempts });
let attempts = 0;
while (attempts < maxAttempts) {
const resp = (await getTask(taskId)) as GetTaskResponse;
if ("error" in resp) {
+ addTaskEvent("failed", taskId, { error: resp.error });
return { ok: false, error: resp.error };
}
const task = resp.data;
@@ -25,15 +28,18 @@ export async function pollTaskUntilSettled(
const result = task?.attributes?.result;
if (!state) {
+ addTaskEvent("failed", taskId, { error: "Task state unavailable" });
return { ok: false, error: "Task state unavailable", task };
}
if (state !== "executing" && state !== "available") {
+ addTaskEvent("completed", taskId, { state });
return { ok: true, state, task, result };
}
attempts++;
await sleep(delayMs);
}
+ addTaskEvent("timeout", taskId, { attempts: attempts });
return { ok: false, error: "Task timeout" };
}
diff --git a/ui/app/(prowler)/error.tsx b/ui/app/(prowler)/error.tsx
index a2ef4650c1..ada2399123 100644
--- a/ui/app/(prowler)/error.tsx
+++ b/ui/app/(prowler)/error.tsx
@@ -1,11 +1,13 @@
"use client";
import { Icon } from "@iconify/react";
+import * as Sentry from "@sentry/nextjs";
import { useEffect } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui";
import { CustomButton } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
+import { SentryErrorSource, SentryErrorType } from "@/sentry";
export default function Error({
error,
@@ -29,9 +31,39 @@ export default function Error({
digest: error.digest,
timestamp: new Date().toISOString(),
});
- // TODO: sent to sentry
+
+ // Send to Sentry with high priority
+ Sentry.captureException(error, {
+ tags: {
+ error_boundary: "app",
+ error_type: SentryErrorType.SERVER_ERROR,
+ error_source: SentryErrorSource.ERROR_BOUNDARY,
+ status_code: "500",
+ digest: error.digest,
+ },
+ level: "error",
+ fingerprint: ["server-error", error.message],
+ contexts: {
+ error_details: {
+ is_server_error: true,
+ timestamp: new Date().toISOString(),
+ },
+ },
+ });
} else {
console.error("Application error:", error);
+
+ // Send other errors to Sentry with normal priority
+ Sentry.captureException(error, {
+ tags: {
+ error_boundary: "app",
+ error_type: SentryErrorType.APPLICATION_ERROR,
+ error_source: SentryErrorSource.ERROR_BOUNDARY,
+ digest: error.digest,
+ },
+ level: "warning",
+ fingerprint: ["app-error", error.message],
+ });
}
}, [error]);
diff --git a/ui/app/(prowler)/layout.tsx b/ui/app/(prowler)/layout.tsx
index c81176ddbe..2c0967e469 100644
--- a/ui/app/(prowler)/layout.tsx
+++ b/ui/app/(prowler)/layout.tsx
@@ -1,5 +1,6 @@
import "@/styles/globals.css";
+import * as Sentry from "@sentry/nextjs";
import { Metadata, Viewport } from "next";
import React from "react";
@@ -22,6 +23,9 @@ export const metadata: Metadata = {
icons: {
icon: "/favicon.ico",
},
+ other: {
+ ...Sentry.getTraceData(),
+ },
};
export const viewport: Viewport = {
diff --git a/ui/app/api/lighthouse/analyst/route.ts b/ui/app/api/lighthouse/analyst/route.ts
index c2cf83333b..2455d30d71 100644
--- a/ui/app/api/lighthouse/analyst/route.ts
+++ b/ui/app/api/lighthouse/analyst/route.ts
@@ -1,4 +1,5 @@
import { toUIMessageStream } from "@ai-sdk/langchain";
+import * as Sentry from "@sentry/nextjs";
import { createUIMessageStreamResponse, UIMessage } from "ai";
import { getLighthouseConfig } from "@/actions/lighthouse/lighthouse";
@@ -6,6 +7,7 @@ import { getErrorMessage } from "@/lib/helper";
import { getCurrentDataSection } from "@/lib/lighthouse/data";
import { convertVercelMessageToLangChainMessage } from "@/lib/lighthouse/utils";
import { initLighthouseWorkflow } from "@/lib/lighthouse/workflow";
+import { SentryErrorSource, SentryErrorType } from "@/sentry";
export async function POST(req: Request) {
try {
@@ -96,7 +98,23 @@ export async function POST(req: Request) {
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
- // For errors, send a plain string that toUIMessageStream will convert to text chunks
+
+ // Capture stream processing errors
+ Sentry.captureException(error, {
+ tags: {
+ api_route: "lighthouse_analyst",
+ error_type: SentryErrorType.STREAM_PROCESSING,
+ error_source: SentryErrorSource.API_ROUTE,
+ },
+ level: "error",
+ contexts: {
+ lighthouse: {
+ event_type: "stream_error",
+ message_count: processedMessages.length,
+ },
+ },
+ });
+
controller.enqueue(`[LIGHTHOUSE_ANALYST_ERROR]: ${errorMessage}`);
controller.close();
}
@@ -109,6 +127,25 @@ export async function POST(req: Request) {
});
} catch (error) {
console.error("Error in POST request:", error);
+
+ // Capture API route errors
+ Sentry.captureException(error, {
+ tags: {
+ api_route: "lighthouse_analyst",
+ error_type: SentryErrorType.REQUEST_PROCESSING,
+ error_source: SentryErrorSource.API_ROUTE,
+ method: "POST",
+ },
+ level: "error",
+ contexts: {
+ request: {
+ method: req.method,
+ url: req.url,
+ headers: Object.fromEntries(req.headers.entries()),
+ },
+ },
+ });
+
return Response.json(
{ error: await getErrorMessage(error) },
{ status: 500 },
diff --git a/ui/app/global-error.tsx b/ui/app/global-error.tsx
new file mode 100644
index 0000000000..d52022bf5d
--- /dev/null
+++ b/ui/app/global-error.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import * as Sentry from "@sentry/nextjs";
+import NextError from "next/error";
+import { useEffect } from "react";
+
+import { SentryErrorSource, SentryErrorType } from "@/sentry";
+
+export default function GlobalError({
+ error,
+ reset: _reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ useEffect(() => {
+ Sentry.captureException(error, {
+ tags: {
+ error_boundary: "global",
+ error_type: SentryErrorType.APPLICATION_ERROR,
+ error_source: SentryErrorSource.ERROR_BOUNDARY,
+ digest: error.digest,
+ },
+ level: "error",
+ contexts: {
+ react: {
+ componentStack: error.stack,
+ },
+ },
+ });
+ }, [error]);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/ui/app/instrumentation.client.ts b/ui/app/instrumentation.client.ts
new file mode 100644
index 0000000000..360c485752
--- /dev/null
+++ b/ui/app/instrumentation.client.ts
@@ -0,0 +1,115 @@
+/**
+ * Client-side Sentry instrumentation
+ *
+ * This file is automatically loaded by Next.js in the browser via the instrumentation hook.
+ * It configures Sentry for client-side error tracking and performance monitoring.
+ *
+ * For server-side configuration, see: instrumentation.ts
+ * For runtime-specific configs, see: sentry/sentry.server.config.ts and sentry/sentry.edge.config.ts
+ */
+
+import { browserTracingIntegration } from "@sentry/browser";
+import * as Sentry from "@sentry/nextjs";
+
+const isDevelopment = process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT === "dev";
+
+/**
+ * Initialize Sentry error tracking and performance monitoring
+ *
+ * This setup includes:
+ * - Performance monitoring with Web Vitals tracking (LCP, FID, CLS, INP)
+ * - Long task detection for UI-blocking operations
+ * - beforeSend hook to filter noise
+ */
+Sentry.init({
+ // 📍 DSN - Data Source Name (identifies your Sentry project)
+ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
+
+ // 🌍 Environment - Separate dev errors from production
+ environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "local",
+
+ // 📦 Release - Track which version has the error
+ release: process.env.NEXT_PUBLIC_PROWLER_RELEASE_VERSION,
+
+ // 🐛 Debug - Detailed logs in development console
+ debug: isDevelopment,
+
+ // 📊 Sample Rates - Performance monitoring
+ // 100% in dev (test everything), 50% in production (balance visibility with costs)
+ tracesSampleRate: isDevelopment ? 1.0 : 0.5,
+ profilesSampleRate: isDevelopment ? 1.0 : 0.5,
+
+ // 🔌 Integrations
+ integrations: [
+ // 📊 Performance Monitoring: Core Web Vitals + RUM
+ // Tracks LCP, FID, CLS, INP
+ // Real User Monitoring captures actual user experience, not synthetic tests
+ browserTracingIntegration({
+ enableLongTask: true, // Detect tasks that block UI (>50ms)
+ enableInp: true, // Interaction to Next Paint (Core Web Vital)
+ }),
+ ],
+
+ // 🎣 beforeSend Hook - Filter or modify events before sending to Sentry
+ ignoreErrors: [
+ // Browser extensions
+ "top.GLOBALS",
+ // Random network errors
+ "Network request failed",
+ "NetworkError",
+ "Failed to fetch",
+ // User canceled actions
+ "AbortError",
+ "Non-Error promise rejection captured",
+ // NextAuth expected errors
+ "NEXT_REDIRECT",
+ // ResizeObserver errors (common browser quirk, not real bugs)
+ "ResizeObserver",
+ ],
+
+ beforeSend(event, hint) {
+ // Filter out noise: ResizeObserver errors (common browser quirk, not real bugs)
+ if (event.message?.includes("ResizeObserver")) {
+ return null; // Don't send to Sentry
+ }
+
+ // Filter out non-actionable errors
+ if (event.exception) {
+ const error = hint.originalException;
+
+ // Don't send cancelled requests
+ if (
+ error &&
+ typeof error === "object" &&
+ "name" in error &&
+ error.name === "AbortError"
+ ) {
+ return null;
+ }
+
+ // Add additional context for API errors
+ if (
+ error &&
+ typeof error === "object" &&
+ "message" in error &&
+ typeof error.message === "string" &&
+ error.message.includes("Request failed")
+ ) {
+ event.tags = {
+ ...event.tags,
+ error_type: "api_error",
+ };
+ }
+ }
+
+ return event; // Send to Sentry
+ },
+});
+
+// 👤 Set user context (identifies who experienced the error)
+// In production, this will be updated after authentication
+if (isDevelopment) {
+ Sentry.setUser({
+ id: "dev-user",
+ });
+}
diff --git a/ui/app/providers.tsx b/ui/app/providers.tsx
index 41157df06c..7bda5d45fe 100644
--- a/ui/app/providers.tsx
+++ b/ui/app/providers.tsx
@@ -1,5 +1,8 @@
"use client";
+// Import Sentry client-side initialization
+import "@/app/instrumentation.client";
+
import { HeroUIProvider } from "@heroui/system";
import { useRouter } from "next/navigation";
import { SessionProvider } from "next-auth/react";
diff --git a/ui/dependency-log.json b/ui/dependency-log.json
index c12ba5e869..720f1818db 100644
--- a/ui/dependency-log.json
+++ b/ui/dependency-log.json
@@ -159,6 +159,14 @@
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
+ {
+ "section": "dependencies",
+ "name": "@sentry/nextjs",
+ "from": "10.11.0",
+ "to": "10.11.0",
+ "strategy": "installed",
+ "generatedAt": "2025-10-22T15:52:15.849Z"
+ },
{
"section": "dependencies",
"name": "@tailwindcss/postcss",
@@ -709,7 +717,7 @@
"from": "3.4.1",
"to": "3.4.1",
"strategy": "installed",
- "generatedAt": "2025-10-22T12:36:37.962Z"
+ "generatedAt": "2025-10-22T15:52:15.849Z"
},
{
"section": "devDependencies",
diff --git a/ui/instrumentation.ts b/ui/instrumentation.ts
new file mode 100644
index 0000000000..3564c2ea57
--- /dev/null
+++ b/ui/instrumentation.ts
@@ -0,0 +1,30 @@
+/**
+ * Next.js Instrumentation Hook
+ *
+ * This file is automatically executed by Next.js at startup to initialize server-side SDKs.
+ *
+ * Configuration Flow:
+ * 1. This file (instrumentation.ts) - Server-side initialization
+ * 2. Runtime-specific configs:
+ * - sentry/sentry.server.config.ts (Node.js runtime)
+ * - sentry/sentry.edge.config.ts (Edge runtime)
+ * 3. Client-side init:
+ * - app/instrumentation.client.ts (Browser/Client)
+ *
+ * @see https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
+ */
+
+import * as Sentry from "@sentry/nextjs";
+
+export async function register() {
+ // The Sentry SDK automatically loads the appropriate config based on runtime
+ if (process.env.NEXT_RUNTIME === "nodejs") {
+ await import("./sentry/sentry.server.config");
+ }
+
+ if (process.env.NEXT_RUNTIME === "edge") {
+ await import("./sentry/sentry.edge.config");
+ }
+}
+
+export const onRequestError = Sentry.captureRequestError;
diff --git a/ui/lib/sentry-breadcrumbs.ts b/ui/lib/sentry-breadcrumbs.ts
new file mode 100644
index 0000000000..5fc2af820a
--- /dev/null
+++ b/ui/lib/sentry-breadcrumbs.ts
@@ -0,0 +1,179 @@
+/**
+ * Sentry Breadcrumb Utilities
+ *
+ * Provides helper functions to add breadcrumbs for tracking critical paths
+ * and user actions throughout the application.
+ *
+ * Usage:
+ * ```typescript
+ * import { addUserAction, addApiCall, addTaskEvent } from '@/lib/sentry-breadcrumbs';
+ *
+ * addUserAction('clicked_create_scan', { provider: 'aws' });
+ * addApiCall('POST /scans', 'success');
+ * addTaskEvent('scan_started', 'scan-123');
+ * ```
+ */
+
+import * as Sentry from "@sentry/nextjs";
+
+export interface BreadcrumbContext {
+ [key: string]: string | number | boolean | undefined;
+}
+
+/**
+ * Add breadcrumb for user actions
+ * @param action - User action identifier
+ * @param context - Additional context data
+ */
+export function addUserAction(action: string, context?: BreadcrumbContext) {
+ Sentry.addBreadcrumb({
+ message: `User action: ${action}`,
+ category: "user.action",
+ level: "info",
+ data: context,
+ });
+}
+
+/**
+ * Add breadcrumb for API calls
+ * @param endpoint - API endpoint (e.g., "GET /scans")
+ * @param status - Status of the call (success, error, timeout)
+ * @param context - Additional context data
+ */
+export function addApiCall(
+ endpoint: string,
+ status: "success" | "error" | "timeout",
+ context?: BreadcrumbContext,
+) {
+ Sentry.addBreadcrumb({
+ message: `API ${endpoint}`,
+ category: "api",
+ level: status === "error" ? "warning" : "info",
+ data: {
+ status,
+ ...context,
+ },
+ });
+}
+
+/**
+ * Add breadcrumb for task events
+ * @param event - Task event (started, completed, failed)
+ * @param taskId - Task identifier
+ * @param context - Additional context data
+ */
+export function addTaskEvent(
+ event: "started" | "completed" | "failed" | "timeout",
+ taskId: string,
+ context?: BreadcrumbContext,
+) {
+ Sentry.addBreadcrumb({
+ message: `Task ${event}: ${taskId}`,
+ category: "task",
+ level: event === "failed" ? "warning" : "info",
+ data: {
+ task_id: taskId,
+ ...context,
+ },
+ });
+}
+
+/**
+ * Add breadcrumb for authentication events
+ * @param event - Auth event (login, logout, signup)
+ * @param context - Additional context data
+ */
+export function addAuthEvent(
+ event: "login" | "logout" | "signup" | "error",
+ context?: BreadcrumbContext,
+) {
+ Sentry.addBreadcrumb({
+ message: `Auth event: ${event}`,
+ category: "auth",
+ level: event === "error" ? "warning" : "info",
+ data: context,
+ });
+}
+
+/**
+ * Add breadcrumb for form submissions
+ * @param formName - Name of the form
+ * @param status - Status of submission
+ * @param context - Additional context data
+ */
+export function addFormSubmission(
+ formName: string,
+ status: "started" | "success" | "error",
+ context?: BreadcrumbContext,
+) {
+ Sentry.addBreadcrumb({
+ message: `Form submission: ${formName}`,
+ category: "form",
+ level: status === "error" ? "warning" : "info",
+ data: {
+ status,
+ ...context,
+ },
+ });
+}
+
+/**
+ * Add breadcrumb for navigation
+ * @param from - Source path
+ * @param to - Destination path
+ */
+export function addNavigation(from: string, to: string) {
+ Sentry.addBreadcrumb({
+ message: `Navigation: ${from} → ${to}`,
+ category: "navigation",
+ level: "info",
+ });
+}
+
+/**
+ * Add breadcrumb for scan operations
+ * @param operation - Operation type (create, start, cancel, etc.)
+ * @param scanId - Scan identifier
+ * @param context - Additional context data
+ */
+export function addScanOperation(
+ operation: "create" | "start" | "cancel" | "pause" | "resume",
+ scanId?: string,
+ context?: BreadcrumbContext,
+) {
+ Sentry.addBreadcrumb({
+ message: `Scan ${operation}${scanId ? `: ${scanId}` : ""}`,
+ category: "scan",
+ level: "info",
+ data: {
+ scan_id: scanId,
+ ...context,
+ },
+ });
+}
+
+/**
+ * Add breadcrumb for data mutations
+ * @param entity - Entity type (provider, scan, role, etc.)
+ * @param action - Action type (create, update, delete)
+ * @param entityId - Entity identifier
+ * @param context - Additional context data
+ */
+export function addDataMutation(
+ entity: string,
+ action: "create" | "update" | "delete",
+ entityId?: string,
+ context?: BreadcrumbContext,
+) {
+ Sentry.addBreadcrumb({
+ message: `Data mutation: ${action} ${entity}${entityId ? ` (${entityId})` : ""}`,
+ category: "data",
+ level: "info",
+ data: {
+ entity,
+ action,
+ entity_id: entityId,
+ ...context,
+ },
+ });
+}
diff --git a/ui/lib/server-actions-helper.ts b/ui/lib/server-actions-helper.ts
index b2c7c3cb7d..49343eaa1d 100644
--- a/ui/lib/server-actions-helper.ts
+++ b/ui/lib/server-actions-helper.ts
@@ -1,8 +1,14 @@
+import * as Sentry from "@sentry/nextjs";
import { revalidatePath } from "next/cache";
+import { SentryErrorSource, SentryErrorType } from "@/sentry";
+
import { getErrorMessage, parseStringify } from "./helper";
-// Helper function to handle API responses consistently
+/**
+ * Helper function to handle API responses consistently
+ * Includes Sentry error tracking for debugging
+ */
export const handleApiResponse = async (
response: Response,
pathToRevalidate?: string,
@@ -29,12 +35,67 @@ export const handleApiResponse = async (
response.statusText ||
"Oops! Something went wrong.";
- //5XX errors
+ // Capture error context for Sentry
+ const errorContext = {
+ status: response.status,
+ statusText: response.statusText,
+ url: response.url,
+ errorDetail,
+ pathToRevalidate,
+ };
+
+ // 5XX errors - Server errors (high priority)
if (response.status >= 500) {
- throw new Error(
+ const serverError = new Error(
errorDetail ||
`Server error (${response.status}): The server encountered an error. Please try again later.`,
);
+
+ Sentry.captureException(serverError, {
+ tags: {
+ api_error: true,
+ status_code: response.status.toString(),
+ error_type: SentryErrorType.SERVER_ERROR,
+ error_source: SentryErrorSource.HANDLE_API_RESPONSE,
+ },
+ level: "error",
+ contexts: {
+ api_response: errorContext,
+ },
+ fingerprint: [
+ "api-server-error",
+ response.status.toString(),
+ response.url,
+ ],
+ });
+
+ throw serverError;
+ }
+
+ // Client errors (4xx) - Only capture unexpected ones
+ if (![401, 403, 404].includes(response.status)) {
+ const clientError = new Error(
+ errorDetail ||
+ `Request failed (${response.status}): ${response.statusText}`,
+ );
+
+ Sentry.captureException(clientError, {
+ tags: {
+ api_error: true,
+ status_code: response.status.toString(),
+ error_type: SentryErrorType.CLIENT_ERROR,
+ error_source: SentryErrorSource.HANDLE_API_RESPONSE,
+ },
+ level: "warning",
+ contexts: {
+ api_response: errorContext,
+ },
+ fingerprint: [
+ "api-client-error",
+ response.status.toString(),
+ response.url,
+ ],
+ });
}
return errorsArray
@@ -76,9 +137,60 @@ export const handleApiResponse = async (
return parse ? parseStringify(data) : data;
};
-// Helper function to handle API errors consistently
+/**
+ * Helper function to handle API errors consistently
+ * Includes Sentry error tracking
+ */
export const handleApiError = (error: unknown): { error: string } => {
console.error(error);
+
+ // Check if this error was already captured by handleApiResponse
+ const isAlreadyCaptured =
+ error instanceof Error &&
+ (error.message.includes("Server error") ||
+ error.message.includes("Request failed"));
+
+ // Only capture if not already captured by handleApiResponse
+ if (!isAlreadyCaptured) {
+ if (error instanceof Error) {
+ // Don't capture expected errors
+ if (
+ !error.message.includes("401") &&
+ !error.message.includes("403") &&
+ !error.message.includes("404")
+ ) {
+ Sentry.captureException(error, {
+ tags: {
+ error_source: SentryErrorSource.HANDLE_API_ERROR,
+ error_type: SentryErrorType.UNEXPECTED_ERROR,
+ },
+ level: "error",
+ contexts: {
+ error_details: {
+ message: error.message,
+ stack: error.stack,
+ },
+ },
+ });
+ }
+ } else {
+ // Capture non-Error objects
+ Sentry.captureMessage(
+ `Non-Error object in handleApiError: ${String(error)}`,
+ {
+ level: "warning",
+ tags: {
+ error_source: SentryErrorSource.HANDLE_API_ERROR,
+ error_type: SentryErrorType.NON_ERROR_OBJECT,
+ },
+ extra: {
+ error: error,
+ },
+ },
+ );
+ }
+ }
+
return {
error: getErrorMessage(error),
};
diff --git a/ui/next.config.js b/ui/next.config.js
index abf95bc6c3..d36dd532ef 100644
--- a/ui/next.config.js
+++ b/ui/next.config.js
@@ -1,19 +1,36 @@
+const { withSentryConfig } = require("@sentry/nextjs");
+
/** @type {import('next').NextConfig} */
// HTTP Security Headers
// 'unsafe-eval' is configured under `script-src` because it is required by NextJS for development mode
const cspHeader = `
default-src 'self';
- script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://www.googletagmanager.com;
- connect-src 'self' https://api.iconify.design https://api.simplesvg.com https://api.unisvg.com https://js.stripe.com https://www.googletagmanager.com;
+ script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://www.googletagmanager.com https://browser.sentry-cdn.com;
+ connect-src 'self' https://api.iconify.design https://api.simplesvg.com https://api.unisvg.com https://js.stripe.com https://www.googletagmanager.com https://*.sentry.io https://*.ingest.sentry.io;
img-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;
font-src 'self';
style-src 'self' 'unsafe-inline';
frame-src 'self' https://js.stripe.com https://www.googletagmanager.com;
frame-ancestors 'none';
+ report-to csp-endpoint;
`;
-module.exports = {
+// Get Sentry CSP report endpoint if DSN is configured
+const getSentryReportEndpoint = () => {
+ if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return null;
+ try {
+ const sentryKey =
+ process.env.NEXT_PUBLIC_SENTRY_DSN.split("@")[0]?.split("//")[1];
+ return sentryKey
+ ? `https://o0.ingest.sentry.io/api/0/security/?sentry_key=${sentryKey}`
+ : null;
+ } catch {
+ return null;
+ }
+};
+
+const nextConfig = {
poweredByHeader: false,
// Use standalone only in production deployments, not for CI/testing
...(process.env.NODE_ENV === "production" &&
@@ -28,24 +45,51 @@ module.exports = {
root: __dirname,
},
async headers() {
+ const sentryEndpoint = getSentryReportEndpoint();
+ const headers = [
+ {
+ key: "Content-Security-Policy",
+ value: cspHeader.replace(/\n/g, ""),
+ },
+ {
+ key: "X-Content-Type-Options",
+ value: "nosniff",
+ },
+ {
+ key: "Referrer-Policy",
+ value: "strict-origin-when-cross-origin",
+ },
+ ];
+
+ // Add Reporting-Endpoints header if Sentry is configured
+ if (sentryEndpoint) {
+ headers.push({
+ key: "Reporting-Endpoints",
+ value: `csp-endpoint="${sentryEndpoint}"`,
+ });
+ }
+
return [
{
source: "/(.*)",
- headers: [
- {
- key: "Content-Security-Policy",
- value: cspHeader.replace(/\n/g, ""),
- },
- {
- key: "X-Content-Type-Options",
- value: "nosniff",
- },
- {
- key: "Referrer-Policy",
- value: "strict-origin-when-cross-origin",
- },
- ],
+ headers,
},
];
},
};
+
+// Sentry configuration options
+const sentryWebpackPluginOptions = {
+ org: process.env.SENTRY_ORG,
+ project: process.env.SENTRY_PROJECT,
+ authToken: process.env.SENTRY_AUTH_TOKEN,
+ silent: true, // Suppresses all logs
+ hideSourceMaps: true, // Hides source maps from generated client bundles
+ disableLogger: true, // Automatically tree-shake Sentry logger statements to reduce bundle size
+ widenClientFileUpload: true, // Upload a larger set of source maps for prettier stack traces
+};
+
+// Export with Sentry only if configuration is available
+module.exports = process.env.SENTRY_DSN
+ ? withSentryConfig(nextConfig, sentryWebpackPluginOptions)
+ : nextConfig;
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 3eb0304232..c3b73351ba 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -29,6 +29,7 @@
"@radix-ui/react-toast": "1.2.14",
"@react-aria/ssr": "3.9.4",
"@react-aria/visually-hidden": "3.8.12",
+ "@sentry/nextjs": "10.11.0",
"@tailwindcss/postcss": "4.1.13",
"@tailwindcss/typography": "0.5.16",
"@tanstack/react-table": "8.21.3",
@@ -262,7 +263,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
@@ -274,31 +274,29 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
- "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
- "dev": true,
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
- "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
- "dev": true,
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.28.3",
+ "@babel/generator": "^7.28.5",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
- "@babel/parser": "^7.28.4",
+ "@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
- "@babel/traverse": "^7.28.4",
- "@babel/types": "^7.28.4",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -314,25 +312,14 @@
"url": "https://opencollective.com/babel"
}
},
- "node_modules/@babel/core/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/generator": {
- "version": "7.28.3",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
- "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
- "dev": true,
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.28.3",
- "@babel/types": "^7.28.2",
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -358,7 +345,6 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.27.2",
@@ -371,29 +357,19 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/helper-create-class-features-plugin": {
- "version": "7.28.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz",
- "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz",
+ "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.3",
- "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-member-expression-to-functions": "^7.28.5",
"@babel/helper-optimise-call-expression": "^7.27.1",
"@babel/helper-replace-supers": "^7.27.1",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
- "@babel/traverse": "^7.28.3",
+ "@babel/traverse": "^7.28.5",
"semver": "^6.3.1"
},
"engines": {
@@ -403,35 +379,24 @@
"@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-member-expression-to-functions": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
- "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz",
+ "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.27.1",
- "@babel/types": "^7.27.1"
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -441,7 +406,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
@@ -455,7 +419,6 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.27.1",
@@ -528,17 +491,15 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
- "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
- "devOptional": true,
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -548,7 +509,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -558,7 +518,6 @@
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
@@ -569,13 +528,12 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
- "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
- "dev": true,
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.4"
+ "@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -634,14 +592,14 @@
}
},
"node_modules/@babel/plugin-transform-typescript": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz",
- "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz",
+ "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.3",
- "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-create-class-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
"@babel/plugin-syntax-typescript": "^7.27.1"
@@ -654,9 +612,9 @@
}
},
"node_modules/@babel/preset-typescript": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz",
- "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz",
+ "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -664,7 +622,7 @@
"@babel/helper-validator-option": "^7.27.1",
"@babel/plugin-syntax-jsx": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.27.1",
- "@babel/plugin-transform-typescript": "^7.27.1"
+ "@babel/plugin-transform-typescript": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -686,7 +644,6 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -698,18 +655,17 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
- "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
- "dev": true,
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.28.3",
+ "@babel/generator": "^7.28.5",
"@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.28.4",
+ "@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
- "@babel/types": "^7.28.4",
+ "@babel/types": "^7.28.5",
"debug": "^4.3.1"
},
"engines": {
@@ -717,14 +673,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
- "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
- "devOptional": true,
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.27.1"
+ "@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -737,9 +692,9 @@
"license": "MIT"
},
"node_modules/@dotenvx/dotenvx": {
- "version": "1.51.0",
- "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.0.tgz",
- "integrity": "sha512-CbMGzyOYSyFF7d4uaeYwO9gpSBzLTnMmSmTVpCZjvpJFV69qYbjYPpzNnCz1mb2wIvEhjWjRwQWuBzTO0jITww==",
+ "version": "1.51.1",
+ "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.1.tgz",
+ "integrity": "sha512-fqcQxcxC4LOaUlW8IkyWw8x0yirlLUkbxohz9OnWvVWjf73J5yyw7jxWnkOJaUKXZotcGEScDox9MU6rSkcDgg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -770,6 +725,19 @@
"node": ">=16"
}
},
+ "node_modules/@dotenvx/dotenvx/node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/@dotenvx/dotenvx/node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -794,24 +762,6 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
- "node_modules/@dotenvx/dotenvx/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
"node_modules/@dotenvx/dotenvx/node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -897,19 +847,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/@dotenvx/dotenvx/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/@dotenvx/dotenvx/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -944,9 +881,9 @@
}
},
"node_modules/@ecies/ciphers": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.4.tgz",
- "integrity": "sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w==",
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.5.tgz",
+ "integrity": "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -959,9 +896,9 @@
}
},
"node_modules/@emnapi/core": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz",
- "integrity": "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz",
+ "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -970,9 +907,9 @@
}
},
"node_modules/@emnapi/runtime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz",
- "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz",
+ "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -3586,6 +3523,15 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@isaacs/fs-minipass/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -3615,6 +3561,17 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -3887,13 +3844,14 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
- "version": "1.20.1",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz",
- "integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==",
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.21.0.tgz",
+ "integrity": "sha512-YFBsXJMFCyI1zP98u7gezMFKX4lgu/XpoZJk7ufI6UlFKXLj2hAMUuRlQX/nrmIPOmhRrG6tw2OQ2ZA/ZlXYpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ajv": "^6.12.6",
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
"content-type": "^1.0.5",
"cors": "^2.8.5",
"cross-spawn": "^7.0.5",
@@ -3908,8 +3866,40 @@
},
"engines": {
"node": ">=18"
+ },
+ "peerDependencies": {
+ "@cfworker/json-schema": "^4.1.1"
+ },
+ "peerDependenciesMeta": {
+ "@cfworker/json-schema": {
+ "optional": true
+ }
}
},
+ "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
@@ -4241,6 +4231,538 @@
"node": ">=8.0.0"
}
},
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.203.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz",
+ "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/context-async-hooks": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz",
+ "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
+ "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation": {
+ "version": "0.203.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz",
+ "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.203.0",
+ "import-in-the-middle": "^1.8.1",
+ "require-in-the-middle": "^7.1.1"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-amqplib": {
+ "version": "0.50.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.50.0.tgz",
+ "integrity": "sha512-kwNs/itehHG/qaQBcVrLNcvXVPW0I4FCOVtw3LHMLdYIqD7GJ6Yv2nX+a4YHjzbzIeRYj8iyMp0Bl7tlkidq5w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^2.0.0",
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-connect": {
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.47.0.tgz",
+ "integrity": "sha512-pjenvjR6+PMRb6/4X85L4OtkQCootgb/Jzh/l/Utu3SJHBid1F+gk9sTGU2FWuhhEfV6P7MZ7BmCdHXQjgJ42g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^2.0.0",
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@types/connect": "3.4.38"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-dataloader": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.21.1.tgz",
+ "integrity": "sha512-hNAm/bwGawLM8VDjKR0ZUDJ/D/qKR3s6lA5NV+btNaPVm2acqhPcT47l2uCVi+70lng2mywfQncor9v8/ykuyw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.203.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-express": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.52.0.tgz",
+ "integrity": "sha512-W7pizN0Wh1/cbNhhTf7C62NpyYw7VfCFTYg0DYieSTrtPBT1vmoSZei19wfKLnrMsz3sHayCg0HxCVL2c+cz5w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^2.0.0",
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-fs": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.23.0.tgz",
+ "integrity": "sha512-Puan+QopWHA/KNYvDfOZN6M/JtF6buXEyD934vrb8WhsX1/FuM7OtoMlQyIqAadnE8FqqDL4KDPiEfCQH6pQcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^2.0.0",
+ "@opentelemetry/instrumentation": "^0.203.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-generic-pool": {
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.47.0.tgz",
+ "integrity": "sha512-UfHqf3zYK+CwDwEtTjaD12uUqGGTswZ7ofLBEdQ4sEJp9GHSSJMQ2hT3pgBxyKADzUdoxQAv/7NqvL42ZI+Qbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.203.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-graphql": {
+ "version": "0.51.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.51.0.tgz",
+ "integrity": "sha512-LchkOu9X5DrXAnPI1+Z06h/EH/zC7D6sA86hhPrk3evLlsJTz0grPrkL/yUJM9Ty0CL/y2HSvmWQCjbJEz/ADg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.203.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-hapi": {
+ "version": "0.50.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.50.0.tgz",
+ "integrity": "sha512-5xGusXOFQXKacrZmDbpHQzqYD1gIkrMWuwvlrEPkYOsjUqGUjl1HbxCsn5Y9bUXOCgP1Lj6A4PcKt1UiJ2MujA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^2.0.0",
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-http": {
+ "version": "0.203.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.203.0.tgz",
+ "integrity": "sha512-y3uQAcCOAwnO6vEuNVocmpVzG3PER6/YZqbPbbffDdJ9te5NkHEkfSMNzlC3+v7KlE+WinPGc3N7MR30G1HY2g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.0.1",
+ "@opentelemetry/instrumentation": "0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0",
+ "forwarded-parse": "2.1.2"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
+ "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-ioredis": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.52.0.tgz",
+ "integrity": "sha512-rUvlyZwI90HRQPYicxpDGhT8setMrlHKokCtBtZgYxQWRF5RBbG4q0pGtbZvd7kyseuHbFpA3I/5z7M8b/5ywg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.204.0",
+ "@opentelemetry/redis-common": "^0.38.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-ioredis/node_modules/@opentelemetry/api-logs": {
+ "version": "0.204.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.204.0.tgz",
+ "integrity": "sha512-DqxY8yoAaiBPivoJD4UtgrMS8gEmzZ5lnaxzPojzLVHBGqPxgWm4zcuvcUHZiqQ6kRX2Klel2r9y8cA2HAtqpw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-ioredis/node_modules/@opentelemetry/instrumentation": {
+ "version": "0.204.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.204.0.tgz",
+ "integrity": "sha512-vV5+WSxktzoMP8JoYWKeopChy6G3HKk4UQ2hESCRDUUTZqQ3+nM3u8noVG0LmNfRWwcFBnbZ71GKC7vaYYdJ1g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.204.0",
+ "import-in-the-middle": "^1.8.1",
+ "require-in-the-middle": "^7.1.1"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-kafkajs": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.13.0.tgz",
+ "integrity": "sha512-FPQyJsREOaGH64hcxlzTsIEQC4DYANgTwHjiB7z9lldmvua1LRMVn3/FfBlzXoqF179B0VGYviz6rn75E9wsDw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.30.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-knex": {
+ "version": "0.48.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.48.0.tgz",
+ "integrity": "sha512-V5wuaBPv/lwGxuHjC6Na2JFRjtPgstw19jTFl1B1b6zvaX8zVDYUDaR5hL7glnQtUSCMktPttQsgK4dhXpddcA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.33.1"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-koa": {
+ "version": "0.51.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.51.0.tgz",
+ "integrity": "sha512-XNLWeMTMG1/EkQBbgPYzCeBD0cwOrfnn8ao4hWgLv0fNCFQu1kCsJYygz2cvKuCs340RlnG4i321hX7R8gj3Rg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^2.0.0",
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-lru-memoizer": {
+ "version": "0.48.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.48.0.tgz",
+ "integrity": "sha512-KUW29wfMlTPX1wFz+NNrmE7IzN7NWZDrmFWHM/VJcmFEuQGnnBuTIdsP55CnBDxKgQ/qqYFp4udQFNtjeFosPw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.203.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mongodb": {
+ "version": "0.56.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.56.0.tgz",
+ "integrity": "sha512-YG5IXUUmxX3Md2buVMvxm9NWlKADrnavI36hbJsihqqvBGsWnIfguf0rUP5Srr0pfPqhQjUP+agLMsvu0GmUpA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mongoose": {
+ "version": "0.50.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.50.0.tgz",
+ "integrity": "sha512-Am8pk1Ct951r4qCiqkBcGmPIgGhoDiFcRtqPSLbJrUZqEPUsigjtMjoWDRLG1Ki1NHgOF7D0H7d+suWz1AAizw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^2.0.0",
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mysql": {
+ "version": "0.49.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.49.0.tgz",
+ "integrity": "sha512-QU9IUNqNsrlfE3dJkZnFHqLjlndiU39ll/YAAEvWE40sGOCi9AtOF6rmEGzJ1IswoZ3oyePV7q2MP8SrhJfVAA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@types/mysql": "2.15.27"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-mysql2": {
+ "version": "0.50.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.50.0.tgz",
+ "integrity": "sha512-PoOMpmq73rOIE3nlTNLf3B1SyNYGsp7QXHYKmeTZZnJ2Ou7/fdURuOhWOI0e6QZ5gSem18IR1sJi6GOULBQJ9g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@opentelemetry/sql-common": "^0.41.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-pg": {
+ "version": "0.55.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.55.0.tgz",
+ "integrity": "sha512-yfJ5bYE7CnkW/uNsnrwouG/FR7nmg09zdk2MSs7k0ZOMkDDAE3WBGpVFFApGgNu2U+gtzLgEzOQG4I/X+60hXw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^2.0.0",
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@opentelemetry/sql-common": "^0.41.0",
+ "@types/pg": "8.15.4",
+ "@types/pg-pool": "2.0.6"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-redis": {
+ "version": "0.51.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.51.0.tgz",
+ "integrity": "sha512-uL/GtBA0u72YPPehwOvthAe+Wf8k3T+XQPBssJmTYl6fzuZjNq8zTfxVFhl9nRFjFVEe+CtiYNT0Q3AyqW1Z0A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/redis-common": "^0.38.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-tedious": {
+ "version": "0.22.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.22.0.tgz",
+ "integrity": "sha512-XrrNSUCyEjH1ax9t+Uo6lv0S2FCCykcF7hSxBMxKf7Xn0bPRxD3KyFUZy25aQXzbbbUHhtdxj3r2h88SfEM3aA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.27.0",
+ "@types/tedious": "^4.0.14"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation-undici": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.14.0.tgz",
+ "integrity": "sha512-2HN+7ztxAReXuxzrtA3WboAKlfP5OsPA57KQn2AdYZbJ3zeRPcLXyW4uO/jpLE6PLm0QRtmeGCmfYpqRlwgSwg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^2.0.0",
+ "@opentelemetry/instrumentation": "^0.203.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.7.0"
+ }
+ },
+ "node_modules/@opentelemetry/redis-common": {
+ "version": "0.38.2",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz",
+ "integrity": "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
+ "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
+ "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.2.0",
+ "@opentelemetry/resources": "2.2.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.37.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz",
+ "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@opentelemetry/sql-common": {
+ "version": "0.41.2",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz",
+ "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^2.0.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0"
+ }
+ },
"node_modules/@panva/hkdf": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
@@ -4279,6 +4801,62 @@
"node": ">=18"
}
},
+ "node_modules/@prisma/instrumentation": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.14.0.tgz",
+ "integrity": "sha512-Po/Hry5bAeunRDq0yAQueKookW3glpP+qjjvvyOfm6dI2KG5/Y6Bgg3ahyWd7B0u2E+Wf9xRk2rtdda7ySgK1A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.8"
+ }
+ },
+ "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": {
+ "version": "0.57.2",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz",
+ "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": {
+ "version": "0.57.2",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz",
+ "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.57.2",
+ "@types/shimmer": "^1.2.0",
+ "import-in-the-middle": "^1.8.1",
+ "require-in-the-middle": "^7.1.1",
+ "semver": "^7.5.2",
+ "shimmer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@prisma/instrumentation/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -7859,6 +8437,340 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
+ "node_modules/@rollup/plugin-commonjs": {
+ "version": "28.0.1",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz",
+ "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==",
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "commondir": "^1.0.1",
+ "estree-walker": "^2.0.2",
+ "fdir": "^6.2.0",
+ "is-reference": "1.2.1",
+ "magic-string": "^0.30.3",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=16.0.0 || 14 >= 14.17"
+ },
+ "peerDependencies": {
+ "rollup": "^2.68.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
+ "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
+ "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
+ "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
+ "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
+ "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
+ "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
+ "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
+ "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
+ "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
+ "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
+ "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
+ "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
+ "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
+ "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
+ "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
+ "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
+ "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
+ "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
+ "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
+ "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
+ "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
+ "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
+ "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -7867,9 +8779,9 @@
"license": "MIT"
},
"node_modules/@rushstack/eslint-patch": {
- "version": "1.14.0",
- "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.14.0.tgz",
- "integrity": "sha512-WJFej426qe4RWOm9MMtP4V3CV4AucXolQty+GRgAWLgQXmpCuwzs7hEpxxhSc/znXUSxum9d/P/32MW0FlAAlA==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.14.1.tgz",
+ "integrity": "sha512-jGTk8UD/RdjsNZW8qq10r0RBvxL8OWtoT+kImlzPDFilmozzM+9QmIJsmze9UiSBrFU45ZxhTYBypn9q9z/VfQ==",
"dev": true,
"license": "MIT"
},
@@ -7880,6 +8792,464 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@sentry-internal/browser-utils": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.11.0.tgz",
+ "integrity": "sha512-fnMlz5ntap6x4vRsLOHwPqXh7t82StgAiRt+EaqcMX0t9l8C0w0df8qwrONKXvE5GdHWTNFJj5qR15FERSkg3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "10.11.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry-internal/feedback": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.11.0.tgz",
+ "integrity": "sha512-ADey51IIaa29kepb8B7aSgSGSrcyT7QZdRsN1rhitefzrruHzpSUci5c2EPIvmWfKJq8Wnvukm9BHXZXAAIOzA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "10.11.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry-internal/replay": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.11.0.tgz",
+ "integrity": "sha512-t4M2bxMp2rKGK/l7bkVWjN+xVw9H9V12jAeXmO/Fskz2RcG1ZNLQnKSx/W/zCRMk8k7xOQFsfiApq+zDN+ziKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/browser-utils": "10.11.0",
+ "@sentry/core": "10.11.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry-internal/replay-canvas": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.11.0.tgz",
+ "integrity": "sha512-brWQ90IYQyZr44IpTprlmvbtz4l2ABzLdpP94Egh12Onf/q6n4CjLKaA25N5kX0uggHqX1Rs7dNaG0mP3ETHhA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/replay": "10.11.0",
+ "@sentry/core": "10.11.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/babel-plugin-component-annotate": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.0.tgz",
+ "integrity": "sha512-3soTX50JPQQ51FSbb4qvNBf4z/yP7jTdn43vMTp9E4IxvJ9HKJR7OEuKkCMszrZmWsVABXl02msqO7QisePdiQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@sentry/browser": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.11.0.tgz",
+ "integrity": "sha512-qemaKCJKJHHCyGBpdLq23xL5u9Xvir20XN7YFTnHcEq4Jvj0GoWsslxKi5cQB2JvpYn62WxTiDgVLeQlleZhSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry-internal/browser-utils": "10.11.0",
+ "@sentry-internal/feedback": "10.11.0",
+ "@sentry-internal/replay": "10.11.0",
+ "@sentry-internal/replay-canvas": "10.11.0",
+ "@sentry/core": "10.11.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.6.0.tgz",
+ "integrity": "sha512-Fub2XQqrS258jjS8qAxLLU1k1h5UCNJ76i8m4qZJJdogWWaF8t00KnnTyp9TEDJzrVD64tRXS8+HHENxmeUo3g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.18.5",
+ "@sentry/babel-plugin-component-annotate": "4.6.0",
+ "@sentry/cli": "^2.57.0",
+ "dotenv": "^16.3.1",
+ "find-up": "^5.0.0",
+ "glob": "^9.3.2",
+ "magic-string": "0.30.8",
+ "unplugin": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/magic-string": {
+ "version": "0.30.8",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
+ "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sentry/cli": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.57.0.tgz",
+ "integrity": "sha512-oC4HPrVIX06GvUTgK0i+WbNgIA9Zl5YEcwf9N4eWFJJmjonr2j4SML9Hn2yNENbUWDgwepy4MLod3P8rM4bk/w==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "https-proxy-agent": "^5.0.0",
+ "node-fetch": "^2.6.7",
+ "progress": "^2.0.3",
+ "proxy-from-env": "^1.1.0",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "sentry-cli": "bin/sentry-cli"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@sentry/cli-darwin": "2.57.0",
+ "@sentry/cli-linux-arm": "2.57.0",
+ "@sentry/cli-linux-arm64": "2.57.0",
+ "@sentry/cli-linux-i686": "2.57.0",
+ "@sentry/cli-linux-x64": "2.57.0",
+ "@sentry/cli-win32-arm64": "2.57.0",
+ "@sentry/cli-win32-i686": "2.57.0",
+ "@sentry/cli-win32-x64": "2.57.0"
+ }
+ },
+ "node_modules/@sentry/cli-darwin": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.57.0.tgz",
+ "integrity": "sha512-v1wYQU3BcCO+Z3OVxxO+EnaW4oQhuOza6CXeYZ0z5ftza9r0QQBLz3bcZKTVta86xraNm0z8GDlREwinyddOxQ==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-arm": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.57.0.tgz",
+ "integrity": "sha512-uNHB8xyygqfMd1/6tFzl9NUkuVefg7jdZtM/vVCQVaF/rJLWZ++Wms+LLhYyKXKN8yd7J9wy7kTEl4Qu4jWbGQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd",
+ "android"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-arm64": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.57.0.tgz",
+ "integrity": "sha512-Kh1jTsMV5Fy/RvB381N/woXe1qclRMqsG6kM3Gq6m6afEF/+k3PyQdNW3HXAola6d63EptokLtxPG2xjWQ+w9Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd",
+ "android"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-i686": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.57.0.tgz",
+ "integrity": "sha512-EYXghoK/tKd0zqz+KD/ewXXE3u1HLCwG89krweveytBy/qw7M5z58eFvw+iGb1Vnbl1f/fRD0G4E0AbEsPfmpg==",
+ "cpu": [
+ "x86",
+ "ia32"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd",
+ "android"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-linux-x64": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.57.0.tgz",
+ "integrity": "sha512-CyZrP/ssHmAPLSzfd4ydy7icDnwmDD6o3QjhkWwVFmCd+9slSBMQxpIqpamZmrWE6X4R+xBRbSUjmdoJoZ5yMw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "linux",
+ "freebsd",
+ "android"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-win32-arm64": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.57.0.tgz",
+ "integrity": "sha512-wji/GGE4Lh5I/dNCsuVbg6fRvttvZRG6db1yPW1BSvQRh8DdnVy1CVp+HMqSq0SRy/S4z60j2u+m4yXMoCL+5g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-win32-i686": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.57.0.tgz",
+ "integrity": "sha512-hWvzyD7bTPh3b55qvJ1Okg3Wbl0Km8xcL6KvS7gfBl6uss+I6RldmQTP0gJKdHSdf/QlJN1FK0b7bLnCB3wHsg==",
+ "cpu": [
+ "x86",
+ "ia32"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/cli-win32-x64": {
+ "version": "2.57.0",
+ "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.57.0.tgz",
+ "integrity": "sha512-QWYV/Y0sbpDSTyA4XQBOTaid4a6H2Iwa1Z8UI+qNxFlk0ADSEgIqo2NrRHDU8iRnghTkecQNX1NTt/7mXN3f/A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@sentry/core": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.11.0.tgz",
+ "integrity": "sha512-39Rxn8cDXConx3+SKOCAhW+/hklM7UDaz+U1OFzFMDlT59vXSpfI6bcXtNiFDrbOxlQ2hX8yAqx8YRltgSftoA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/nextjs": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-10.11.0.tgz",
+ "integrity": "sha512-oMRmRW982H6kNlUHNij5QAro8Kbi43r3VrcrKtrx7LgjHOUTFUvZmJeynC+T+PcMgLhQNvCC3JgzOhfSqxOChg==",
+ "license": "MIT",
+ "dependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/semantic-conventions": "^1.34.0",
+ "@rollup/plugin-commonjs": "28.0.1",
+ "@sentry-internal/browser-utils": "10.11.0",
+ "@sentry/bundler-plugin-core": "^4.3.0",
+ "@sentry/core": "10.11.0",
+ "@sentry/node": "10.11.0",
+ "@sentry/opentelemetry": "10.11.0",
+ "@sentry/react": "10.11.0",
+ "@sentry/vercel-edge": "10.11.0",
+ "@sentry/webpack-plugin": "^4.3.0",
+ "chalk": "3.0.0",
+ "resolve": "1.22.8",
+ "rollup": "^4.35.0",
+ "stacktrace-parser": "^0.1.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0"
+ }
+ },
+ "node_modules/@sentry/node": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.11.0.tgz",
+ "integrity": "sha512-Tbcjr3iQAEjYi7/QIpdS8afv/LU1TwDTiy5x87MSpVEoeFcZ7f2iFC4GV0fhB3p4qDuFdL2JGVsIIrzapp8Y4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^2.0.0",
+ "@opentelemetry/core": "^2.0.0",
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/instrumentation-amqplib": "0.50.0",
+ "@opentelemetry/instrumentation-connect": "0.47.0",
+ "@opentelemetry/instrumentation-dataloader": "0.21.1",
+ "@opentelemetry/instrumentation-express": "0.52.0",
+ "@opentelemetry/instrumentation-fs": "0.23.0",
+ "@opentelemetry/instrumentation-generic-pool": "0.47.0",
+ "@opentelemetry/instrumentation-graphql": "0.51.0",
+ "@opentelemetry/instrumentation-hapi": "0.50.0",
+ "@opentelemetry/instrumentation-http": "0.203.0",
+ "@opentelemetry/instrumentation-ioredis": "0.52.0",
+ "@opentelemetry/instrumentation-kafkajs": "0.13.0",
+ "@opentelemetry/instrumentation-knex": "0.48.0",
+ "@opentelemetry/instrumentation-koa": "0.51.0",
+ "@opentelemetry/instrumentation-lru-memoizer": "0.48.0",
+ "@opentelemetry/instrumentation-mongodb": "0.56.0",
+ "@opentelemetry/instrumentation-mongoose": "0.50.0",
+ "@opentelemetry/instrumentation-mysql": "0.49.0",
+ "@opentelemetry/instrumentation-mysql2": "0.50.0",
+ "@opentelemetry/instrumentation-pg": "0.55.0",
+ "@opentelemetry/instrumentation-redis": "0.51.0",
+ "@opentelemetry/instrumentation-tedious": "0.22.0",
+ "@opentelemetry/instrumentation-undici": "0.14.0",
+ "@opentelemetry/resources": "^2.0.0",
+ "@opentelemetry/sdk-trace-base": "^2.0.0",
+ "@opentelemetry/semantic-conventions": "^1.34.0",
+ "@prisma/instrumentation": "6.14.0",
+ "@sentry/core": "10.11.0",
+ "@sentry/node-core": "10.11.0",
+ "@sentry/opentelemetry": "10.11.0",
+ "import-in-the-middle": "^1.14.2",
+ "minimatch": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/node-core": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.11.0.tgz",
+ "integrity": "sha512-dkVZ06F+W5W0CsD47ATTTOTTocmccT/ezrF9idspQq+HVOcjoKSU60WpWo22NjtVNdSYKLnom0q1LKRoaRA/Ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "10.11.0",
+ "@sentry/opentelemetry": "10.11.0",
+ "import-in-the-middle": "^1.14.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0",
+ "@opentelemetry/core": "^1.30.1 || ^2.0.0",
+ "@opentelemetry/instrumentation": ">=0.57.1 <1",
+ "@opentelemetry/resources": "^1.30.1 || ^2.0.0",
+ "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0",
+ "@opentelemetry/semantic-conventions": "^1.34.0"
+ }
+ },
+ "node_modules/@sentry/opentelemetry": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.11.0.tgz",
+ "integrity": "sha512-BY2SsVlRKICzNUO9atUy064BZqYnhV5A/O+JjEx0kj7ylq+oZd++zmGkks00rSwaJE220cVcVhpwqxcFUpc2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/core": "10.11.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0",
+ "@opentelemetry/core": "^1.30.1 || ^2.0.0",
+ "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0",
+ "@opentelemetry/semantic-conventions": "^1.34.0"
+ }
+ },
+ "node_modules/@sentry/react": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.11.0.tgz",
+ "integrity": "sha512-bE4lJ5Ni/n9JUdLWGG99yucY0/zOUXjKl9gfSTkvUvOiAIX/bY0Y4WgOqeWySvbMz679ZdOwF34k8RA/gI7a8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/browser": "10.11.0",
+ "@sentry/core": "10.11.0",
+ "hoist-non-react-statics": "^3.3.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.14.0 || 17.x || 18.x || 19.x"
+ }
+ },
+ "node_modules/@sentry/vercel-edge": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-10.11.0.tgz",
+ "integrity": "sha512-jAsJ8RbbF2JWj2wnXfd6BwWxCR6GBITMtlaoWc7pG22HknEtoH15dKsQC3Ew5r/KRcofr2e+ywdnBn5CPr1Pbg==",
+ "license": "MIT",
+ "dependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/resources": "^2.0.0",
+ "@sentry/core": "10.11.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/webpack-plugin": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-4.6.0.tgz",
+ "integrity": "sha512-i9Yy2kXCbFKlRST09fV1HsI0naJAfeXxoiUPyh5iCgSo2w7ZwEUlk0tJhupnHZzfSa3OSg01+vVNeeyLYM4tdA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/bundler-plugin-core": "4.6.0",
+ "unplugin": "1.0.1",
+ "uuid": "^9.0.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "webpack": ">=4.40.0"
+ }
+ },
+ "node_modules/@sentry/webpack-plugin/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
@@ -8321,11 +9691,11 @@
}
},
"node_modules/@ts-morph/common/node_modules/minimatch": {
- "version": "10.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
- "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
+ "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
@@ -8346,6 +9716,15 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/d3": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
@@ -8630,6 +10009,28 @@
"@types/ms": "*"
}
},
+ "node_modules/@types/eslint": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
+ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/eslint-scope": {
+ "version": "3.7.7",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/eslint": "*",
+ "@types/estree": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -8695,13 +10096,41 @@
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
+ "node_modules/@types/mysql": {
+ "version": "2.15.27",
+ "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz",
+ "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "20.5.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz",
"integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/@types/pg": {
+ "version": "8.15.4",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz",
+ "integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "node_modules/@types/pg-pool": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz",
+ "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/pg": "*"
+ }
+ },
"node_modules/@types/react": {
"version": "19.1.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
@@ -8727,6 +10156,12 @@
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"license": "MIT"
},
+ "node_modules/@types/shimmer": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
+ "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
+ "license": "MIT"
+ },
"node_modules/@types/statuses": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
@@ -8734,6 +10169,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/tedious": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
+ "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/topojson-client": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@types/topojson-client/-/topojson-client-3.1.5.tgz",
@@ -8919,6 +10363,19 @@
}
}
},
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@typescript-eslint/utils": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
@@ -9235,6 +10692,181 @@
"win32"
]
},
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+ "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+ "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+ "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+ "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/wasm-gen": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+ "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/helper-wasm-section": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-opt": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1",
+ "@webassemblyjs/wast-printer": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+ "license": "BSD-3-Clause",
+ "peer": true
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+ "license": "Apache-2.0",
+ "peer": true
+ },
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -9253,7 +10885,6 @@
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
- "dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -9262,6 +10893,28 @@
"node": ">=0.4.0"
}
},
+ "node_modules/acorn-import-attributes": {
+ "version": "1.9.5",
+ "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
+ "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^8"
+ }
+ },
+ "node_modules/acorn-import-phases": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
+ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "peerDependencies": {
+ "acorn": "^8.14.0"
+ }
+ },
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@@ -9273,13 +10926,15 @@
}
},
"node_modules/agent-base": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
- "dev": true,
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
"engines": {
- "node": ">= 14"
+ "node": ">= 6.0.0"
}
},
"node_modules/ai": {
@@ -9317,6 +10972,48 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/alert": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/alert/-/alert-6.0.2.tgz",
@@ -9328,9 +11025,9 @@
}
},
"node_modules/ansi-escapes": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz",
- "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
+ "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9375,6 +11072,31 @@
"node": ">=14"
}
},
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -9701,7 +11423,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
@@ -9725,15 +11446,26 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.19",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.19.tgz",
- "integrity": "sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==",
- "dev": true,
+ "version": "2.8.23",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz",
+ "integrity": "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
},
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@@ -9759,7 +11491,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -9769,7 +11500,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -9779,10 +11509,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.26.3",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
- "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
- "dev": true,
+ "version": "4.27.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
+ "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
"funding": [
{
"type": "opencollective",
@@ -9799,11 +11528,11 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.8.9",
- "caniuse-lite": "^1.0.30001746",
- "electron-to-chromium": "^1.5.227",
- "node-releases": "^2.0.21",
- "update-browserslist-db": "^1.1.3"
+ "baseline-browser-mapping": "^2.8.19",
+ "caniuse-lite": "^1.0.30001751",
+ "electron-to-chromium": "^1.5.238",
+ "node-releases": "^2.0.26",
+ "update-browserslist-db": "^1.1.4"
},
"bin": {
"browserslist": "cli.js"
@@ -9812,6 +11541,13 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -9895,9 +11631,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001751",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
- "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
+ "version": "1.0.30001753",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz",
+ "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==",
"funding": [
{
"type": "opencollective",
@@ -9925,19 +11661,16 @@
}
},
"node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
+ "node": ">=8"
}
},
"node_modules/chalk/node_modules/ansi-styles": {
@@ -9995,6 +11728,42 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -10004,6 +11773,22 @@
"node": ">=18"
}
},
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
+ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
+ "license": "MIT"
+ },
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -10248,6 +12033,12 @@
"node": ">= 10"
}
},
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+ "license": "MIT"
+ },
"node_modules/compute-scroll-into-view": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
@@ -10297,7 +12088,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true,
"license": "MIT"
},
"node_modules/cookie": {
@@ -11097,10 +12887,9 @@
}
},
"node_modules/dotenv": {
- "version": "17.2.3",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
- "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
- "dev": true,
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -11150,10 +12939,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.238",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.238.tgz",
- "integrity": "sha512-khBdc+w/Gv+cS8e/Pbnaw/FXcBUeKrRVik9IxfXtgREOWyJhR4tj43n3amkVogJ/yeQUqzkrZcFhtIxIdqmmcQ==",
- "dev": true,
+ "version": "1.5.244",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz",
+ "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==",
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -11345,6 +13133,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -11409,7 +13204,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -11735,16 +13529,6 @@
"node": "*"
}
},
- "node_modules/eslint-plugin-import/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/eslint-plugin-jsx-a11y": {
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
@@ -11844,16 +13628,6 @@
"node": "*"
}
},
- "node_modules/eslint-plugin-node/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/eslint-plugin-prettier": {
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz",
@@ -11993,16 +13767,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-plugin-react/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/eslint-plugin-security": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-3.0.1.tgz",
@@ -12117,6 +13881,22 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -12128,6 +13908,23 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -12190,7 +13987,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
@@ -12203,7 +13999,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -12219,6 +14014,12 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -12246,6 +14047,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
@@ -12361,7 +14172,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/fast-diff": {
@@ -12424,6 +14234,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -12434,6 +14260,23 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
@@ -12491,7 +14334,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -12522,7 +14364,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
@@ -12605,6 +14446,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/forwarded-parse": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz",
+ "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==",
+ "license": "MIT"
+ },
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -12675,14 +14522,12 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -12697,7 +14542,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -12762,7 +14606,6 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -12897,22 +14740,18 @@
}
},
"node_modules/glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "deprecated": "Glob versions prior to v9 are no longer supported",
- "dev": true,
+ "version": "9.3.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
+ "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
+ "minimatch": "^8.0.2",
+ "minipass": "^4.2.4",
+ "path-scurry": "^1.6.1"
},
"engines": {
- "node": "*"
+ "node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -12931,28 +14770,26 @@
"node": ">=10.13.0"
}
},
- "node_modules/glob/node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "license": "BSD-2-Clause",
+ "peer": true
},
"node_modules/glob/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz",
+ "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==",
"license": "ISC",
"dependencies": {
- "brace-expansion": "^1.1.7"
+ "brace-expansion": "^2.0.1"
},
"engines": {
- "node": "*"
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/globals": {
@@ -13036,9 +14873,9 @@
"license": "MIT"
},
"node_modules/graphql": {
- "version": "16.11.0",
- "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz",
- "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==",
+ "version": "16.12.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz",
+ "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -13129,7 +14966,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -13202,6 +15038,15 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -13240,17 +15085,16 @@
}
},
"node_modules/https-proxy-agent": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
- "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
- "dev": true,
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
- "agent-base": "^7.1.2",
+ "agent-base": "6",
"debug": "4"
},
"engines": {
- "node": ">= 14"
+ "node": ">= 6"
}
},
"node_modules/human-signals": {
@@ -13318,6 +15162,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/import-in-the-middle": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz",
+ "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "acorn-import-attributes": "^1.9.5",
+ "cjs-module-lexer": "^1.2.2",
+ "module-details-from-path": "^1.0.3"
+ }
+ },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -13348,9 +15204,9 @@
"license": "ISC"
},
"node_modules/inline-style-parser": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
- "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.6.tgz",
+ "integrity": "sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg==",
"license": "MIT"
},
"node_modules/input-otp": {
@@ -13494,6 +15350,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-boolean-object": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
@@ -13521,6 +15389,19 @@
"semver": "^7.7.1"
}
},
+ "node_modules/is-bun-module/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
@@ -13538,7 +15419,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@@ -13599,7 +15479,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -13658,7 +15537,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -13727,7 +15605,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -13792,6 +15669,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/is-reference": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
+ "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -13987,7 +15873,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
"license": "ISC"
},
"node_modules/iterator.prototype": {
@@ -14008,6 +15893,37 @@
"node": ">= 0.4"
}
},
+ "node_modules/jest-worker": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -14057,7 +15973,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -14077,7 +15992,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
- "dev": true,
"license": "MIT"
},
"node_modules/json-schema": {
@@ -14104,7 +16018,6 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
@@ -14172,9 +16085,9 @@
}
},
"node_modules/langsmith": {
- "version": "0.3.74",
- "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.74.tgz",
- "integrity": "sha512-ZuW3Qawz8w88XcuCRH91yTp6lsdGuwzRqZ5J0Hf5q/AjMz7DwcSv0MkE6V5W+8hFMI850QZN2Wlxwm3R9lHlZg==",
+ "version": "0.3.77",
+ "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.77.tgz",
+ "integrity": "sha512-wbS/9IX/hOAsOEOtPj8kCS8H0tFHaelwQ97gTONRtIfoPPLd9MMUmhk0KQB5DdsGAI5abg966+f0dZ/B+YRRzg==",
"license": "MIT",
"dependencies": {
"@types/uuid": "^10.0.0",
@@ -14206,6 +16119,49 @@
}
}
},
+ "node_modules/langsmith/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/langsmith/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/langsmith/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/langsmith/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
@@ -14570,11 +16526,24 @@
"node": ">=18.0.0"
}
},
+ "node_modules/loader-runner": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
+ "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6.11.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
@@ -14774,7 +16743,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
@@ -14790,9 +16758,9 @@
}
},
"node_modules/magic-string": {
- "version": "0.30.19",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
- "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@@ -15000,7 +16968,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
- "dev": true,
"license": "MIT"
},
"node_modules/merge2": {
@@ -15469,6 +17436,19 @@
"node": ">=8.6"
}
},
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
@@ -15522,7 +17502,6 @@
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -15545,12 +17524,12 @@
}
},
"node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz",
+ "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==",
"license": "ISC",
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": ">=8"
}
},
"node_modules/minizlib": {
@@ -15565,6 +17544,21 @@
"node": ">= 18"
}
},
+ "node_modules/minizlib/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/module-details-from-path": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
+ "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
+ "license": "MIT"
+ },
"node_modules/motion-dom": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz",
@@ -15724,6 +17718,13 @@
"node": ">= 0.6"
}
},
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/next": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz",
@@ -16212,6 +18213,19 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/next/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/next/node_modules/sharp": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz",
@@ -16277,31 +18291,40 @@
}
},
"node_modules/node-fetch": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
- "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
- "dev": true,
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
- "data-uri-to-buffer": "^4.0.0",
- "fetch-blob": "^3.1.4",
- "formdata-polyfill": "^4.0.10"
+ "whatwg-url": "^5.0.0"
},
"engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ "node": "4.x || >=6.0.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/node-fetch"
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
}
},
"node_modules/node-releases": {
- "version": "2.0.26",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
- "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==",
- "dev": true,
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"license": "MIT"
},
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
@@ -16643,7 +18666,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
@@ -16659,7 +18681,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
@@ -16816,7 +18837,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -16846,9 +18866,39 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/path-scurry/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
"node_modules/path-to-regexp": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
@@ -16866,6 +18916,37 @@
"node": ">=8"
}
},
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
+ "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -16873,13 +18954,12 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"engines": {
- "node": ">=8.6"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -16999,6 +19079,45 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
+ "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/preact": {
"version": "10.24.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
@@ -17160,6 +19279,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -17219,6 +19347,12 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -17343,6 +19477,16 @@
}
}
},
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -17573,6 +19717,30 @@
"react-dom": ">=16.6.0"
}
},
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/readdirp/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/recast": {
"version": "0.23.11",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz",
@@ -17744,23 +19912,42 @@
"node": ">=0.10.0"
}
},
- "node_modules/resolve": {
- "version": "1.22.11",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
- "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
- "dev": true,
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-in-the-middle": {
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz",
+ "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==",
"license": "MIT",
"dependencies": {
- "is-core-module": "^2.16.1",
+ "debug": "^4.3.5",
+ "module-details-from-path": "^1.0.3",
+ "resolve": "^1.22.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.8",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+ "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
- "engines": {
- "node": ">= 0.4"
- },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -17869,12 +20056,99 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/rimraf/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/rimraf/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/robust-predicates": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
"license": "Unlicense"
},
+ "node_modules/rollup": {
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
+ "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.52.5",
+ "@rollup/rollup-android-arm64": "4.52.5",
+ "@rollup/rollup-darwin-arm64": "4.52.5",
+ "@rollup/rollup-darwin-x64": "4.52.5",
+ "@rollup/rollup-freebsd-arm64": "4.52.5",
+ "@rollup/rollup-freebsd-x64": "4.52.5",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
+ "@rollup/rollup-linux-arm-musleabihf": "4.52.5",
+ "@rollup/rollup-linux-arm64-gnu": "4.52.5",
+ "@rollup/rollup-linux-arm64-musl": "4.52.5",
+ "@rollup/rollup-linux-loong64-gnu": "4.52.5",
+ "@rollup/rollup-linux-ppc64-gnu": "4.52.5",
+ "@rollup/rollup-linux-riscv64-gnu": "4.52.5",
+ "@rollup/rollup-linux-riscv64-musl": "4.52.5",
+ "@rollup/rollup-linux-s390x-gnu": "4.52.5",
+ "@rollup/rollup-linux-x64-gnu": "4.52.5",
+ "@rollup/rollup-linux-x64-musl": "4.52.5",
+ "@rollup/rollup-openharmony-arm64": "4.52.5",
+ "@rollup/rollup-win32-arm64-msvc": "4.52.5",
+ "@rollup/rollup-win32-ia32-msvc": "4.52.5",
+ "@rollup/rollup-win32-x64-gnu": "4.52.5",
+ "@rollup/rollup-win32-x64-msvc": "4.52.5",
+ "fsevents": "~2.3.2"
+ }
+ },
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -17967,7 +20241,6 @@
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -18036,10 +20309,10 @@
"license": "MIT"
},
"node_modules/sax": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
- "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
- "license": "ISC"
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.2.tgz",
+ "integrity": "sha512-FySGAa0RGcFiN6zfrO9JvK1r7TB59xuzCcTHOBXBNoKgDejlOQCR2KL/FGk3/iDlsqyYg1ELZpOmlg09B01Czw==",
+ "license": "BlueOak-1.0.0"
},
"node_modules/scheduler": {
"version": "0.27.0",
@@ -18047,6 +20320,81 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
+ "node_modules/schema-utils": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv-formats": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/schema-utils/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/scroll-into-view-if-needed": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.0.10.tgz",
@@ -18057,15 +20405,12 @@
}
},
"node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
}
},
"node_modules/send": {
@@ -18091,6 +20436,16 @@
"node": ">= 18"
}
},
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
"node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
@@ -18211,10 +20566,20 @@
"shadcn": "dist/index.js"
}
},
+ "node_modules/shadcn/node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/shadcn/node_modules/commander": {
- "version": "14.0.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz",
- "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==",
+ "version": "14.0.2",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
+ "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -18295,6 +20660,20 @@
"node": ">= 6"
}
},
+ "node_modules/shadcn/node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/shadcn/node_modules/human-signals": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
@@ -18318,6 +20697,25 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/shadcn/node_modules/node-fetch": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/node-fetch"
+ }
+ },
"node_modules/shadcn/node_modules/npm-run-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
@@ -18464,6 +20862,18 @@
"@img/sharp-win32-x64": "0.33.5"
}
},
+ "node_modules/sharp/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -18487,6 +20897,12 @@
"node": ">=8"
}
},
+ "node_modules/shimmer": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
+ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@@ -18648,7 +21064,6 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -18663,6 +21078,17 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
@@ -18680,6 +21106,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/stacktrace-parser": {
+ "version": "0.1.11",
+ "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz",
+ "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.7.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/stacktrace-parser/node_modules/type-fest": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
+ "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -18983,21 +21430,21 @@
}
},
"node_modules/style-to-js": {
- "version": "1.1.18",
- "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.18.tgz",
- "integrity": "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==",
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.19.tgz",
+ "integrity": "sha512-Ev+SgeqiNGT1ufsXyVC5RrJRXdrkRJ1Gol9Qw7Pb72YCKJXrBvP0ckZhBeVSrw2m06DJpei2528uIpjMb4TsoQ==",
"license": "MIT",
"dependencies": {
- "style-to-object": "1.0.11"
+ "style-to-object": "1.0.12"
}
},
"node_modules/style-to-object": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.11.tgz",
- "integrity": "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==",
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.12.tgz",
+ "integrity": "sha512-ddJqYnoT4t97QvN2C95bCgt+m7AAgXjVnkk/jxAfmp7EAB8nnqqZYEbMd3em7/vEomDb2LAQKAy1RFfv41mdNw==",
"license": "MIT",
"dependencies": {
- "inline-style-parser": "0.2.4"
+ "inline-style-parser": "0.2.6"
}
},
"node_modules/styled-jsx": {
@@ -19039,7 +21486,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -19144,10 +21590,10 @@
}
},
"node_modules/tar": {
- "version": "7.5.1",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz",
- "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
- "license": "ISC",
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
+ "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
@@ -19159,6 +21605,15 @@
"node": ">=18"
}
},
+ "node_modules/tar/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
"node_modules/tar/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
@@ -19168,6 +21623,67 @@
"node": ">=18"
}
},
+ "node_modules/terser": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
+ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.3.14",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
+ "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^4.3.0",
+ "serialize-javascript": "^6.0.2",
+ "terser": "^5.31.1"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -19223,37 +21739,6 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
- "node_modules/tinyglobby/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/tldts": {
"version": "7.0.17",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz",
@@ -19278,7 +21763,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -19330,6 +21814,12 @@
"node": ">=16"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -19687,6 +22177,18 @@
"node": ">= 0.8"
}
},
+ "node_modules/unplugin": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz",
+ "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==",
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.8.1",
+ "chokidar": "^3.5.3",
+ "webpack-sources": "^3.2.3",
+ "webpack-virtual-modules": "^0.5.0"
+ }
+ },
"node_modules/unrs-resolver": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
@@ -19733,10 +22235,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
- "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
- "dev": true,
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
+ "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
"funding": [
{
"type": "opencollective",
@@ -19949,6 +22450,20 @@
"d3-timer": "^3.0.1"
}
},
+ "node_modules/watchpack": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
+ "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
@@ -19959,11 +22474,137 @@
"node": ">= 8"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/webpack": {
+ "version": "5.102.1",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
+ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/eslint-scope": "^3.7.7",
+ "@types/estree": "^1.0.8",
+ "@types/json-schema": "^7.0.15",
+ "@webassemblyjs/ast": "^1.14.1",
+ "@webassemblyjs/wasm-edit": "^1.14.1",
+ "@webassemblyjs/wasm-parser": "^1.14.1",
+ "acorn": "^8.15.0",
+ "acorn-import-phases": "^1.0.3",
+ "browserslist": "^4.26.3",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.17.3",
+ "es-module-lexer": "^1.2.1",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.11",
+ "json-parse-even-better-errors": "^2.3.1",
+ "loader-runner": "^4.2.0",
+ "mime-types": "^2.1.27",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^4.3.3",
+ "tapable": "^2.3.0",
+ "terser-webpack-plugin": "^5.3.11",
+ "watchpack": "^2.4.4",
+ "webpack-sources": "^3.3.3"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
+ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack-virtual-modules": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
+ "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
+ "license": "MIT"
+ },
+ "node_modules/webpack/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/webpack/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/webpack/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/webpack/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -20169,6 +22810,15 @@
"node": ">=4.0"
}
},
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -20183,7 +22833,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true,
"license": "ISC"
},
"node_modules/yaml": {
@@ -20264,7 +22913,6 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
diff --git a/ui/package.json b/ui/package.json
index e89a07c1bf..28d507e9bc 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -43,6 +43,7 @@
"@radix-ui/react-toast": "1.2.14",
"@react-aria/ssr": "3.9.4",
"@react-aria/visually-hidden": "3.8.12",
+ "@sentry/nextjs": "10.11.0",
"@tailwindcss/postcss": "4.1.13",
"@tailwindcss/typography": "0.5.16",
"@tanstack/react-table": "8.21.3",
diff --git a/ui/sentry/README.md b/ui/sentry/README.md
new file mode 100644
index 0000000000..440ff8553b
--- /dev/null
+++ b/ui/sentry/README.md
@@ -0,0 +1,52 @@
+# Sentry Error Tracking Configuration
+
+This folder contains all Sentry-related configuration and utilities for the Prowler UI.
+
+## Files
+
+- `sentry.server.config.ts` - Server-side error tracking configuration
+- `sentry.edge.config.ts` - Edge runtime error tracking configuration
+- `utils.ts` - Enums for standardized error types and sources
+- `index.ts` - Main export file
+
+## Client Configuration
+
+The client-side configuration is located in `app/instrumentation.client.ts` following Next.js conventions.
+
+## Usage
+
+```typescript
+// Import Sentry enums for error categorization
+import { SentryErrorType, SentryErrorSource } from "@/sentry";
+
+// Use in error handling
+Sentry.captureException(error, {
+ tags: {
+ error_type: SentryErrorType.SERVER_ERROR,
+ error_source: SentryErrorSource.API_ROUTE,
+ },
+});
+```
+
+## Environment Variables
+
+Required environment variables (add to `.env`):
+
+```env
+SENTRY_DSN=https://YOUR_KEY@o0.ingest.sentry.io/0
+NEXT_PUBLIC_SENTRY_DSN=https://YOUR_KEY@o0.ingest.sentry.io/0
+SENTRY_ORG=your-org-slug
+SENTRY_PROJECT=your-project-slug
+SENTRY_AUTH_TOKEN=sntrys_YOUR_AUTH_TOKEN
+SENTRY_ENVIRONMENT=development
+NEXT_PUBLIC_SENTRY_ENVIRONMENT=development
+```
+
+## Ignored Errors
+
+The following errors are intentionally ignored as they are expected behavior:
+- `NEXT_REDIRECT` - Next.js redirect mechanism
+- `NEXT_NOT_FOUND` - Next.js 404 handling
+- `401` - Unauthorized (expected when token expires)
+- `403` - Forbidden (expected for permission checks)
+- `404` - Not Found (expected for missing resources)
\ No newline at end of file
diff --git a/ui/sentry/index.ts b/ui/sentry/index.ts
new file mode 100644
index 0000000000..dca76ca0e0
--- /dev/null
+++ b/ui/sentry/index.ts
@@ -0,0 +1,2 @@
+// Re-export all Sentry utilities
+export * from "./utils";
diff --git a/ui/sentry/sentry.edge.config.ts b/ui/sentry/sentry.edge.config.ts
new file mode 100644
index 0000000000..eefaa5c527
--- /dev/null
+++ b/ui/sentry/sentry.edge.config.ts
@@ -0,0 +1,64 @@
+import * as Sentry from "@sentry/nextjs";
+
+const isProduction = process.env.SENTRY_ENVIRONMENT === "pro";
+
+/**
+ * Edge runtime Sentry configuration
+ *
+ * Edge runtime has stricter constraints than Node.js:
+ * - Limited execution time (~10-30 seconds)
+ * - Lower memory availability
+ * - Reduced sample rates to minimize overhead
+ * - No complex integrations
+ */
+Sentry.init({
+ // 📍 DSN - Data Source Name (identifies your Sentry project)
+ dsn: process.env.SENTRY_DSN,
+
+ // 🌍 Environment configuration
+ environment: process.env.SENTRY_ENVIRONMENT || "local",
+
+ // 📦 Release tracking
+ release: process.env.SENTRY_RELEASE,
+
+ // 📊 Sample Rates - Reduced for edge runtime constraints
+ // 50% in dev, 25% in production (edge has lower overhead limits than server)
+ tracesSampleRate: isProduction ? 0.25 : 0.5,
+
+ // 🔌 Integrations - Edge runtime doesn't support all integrations
+ integrations: [],
+
+ // 🎣 Filter expected errors - Don't send noise to Sentry
+ ignoreErrors: [
+ // NextAuth redirect errors - Expected behavior in auth flow
+ "NEXT_REDIRECT",
+ "NEXT_NOT_FOUND",
+ // Expected HTTP errors - Expected when users lack permissions
+ "401", // Unauthorized - expected when token expires
+ "403", // Forbidden - expected when no permissions
+ "404", // Not Found - expected for missing resources
+ ],
+
+ beforeSend(event, hint) {
+ // Add edge runtime context for debugging
+ event.tags = {
+ ...event.tags,
+ runtime: "edge",
+ };
+
+ const error = hint.originalException;
+
+ // Don't send NextAuth expected errors
+ if (
+ error &&
+ typeof error === "object" &&
+ "message" in error &&
+ typeof error.message === "string" &&
+ error.message.includes("NEXT_REDIRECT")
+ ) {
+ return null;
+ }
+
+ return event;
+ },
+});
diff --git a/ui/sentry/sentry.server.config.ts b/ui/sentry/sentry.server.config.ts
new file mode 100644
index 0000000000..4045dd1534
--- /dev/null
+++ b/ui/sentry/sentry.server.config.ts
@@ -0,0 +1,80 @@
+import * as Sentry from "@sentry/nextjs";
+
+const isProduction = process.env.SENTRY_ENVIRONMENT === "pro";
+
+/**
+ * Server-side Sentry configuration
+ *
+ * This setup includes:
+ * - Performance monitoring for server-side operations
+ * - Error tracking for API routes and server actions
+ * - beforeSend hook to filter noise and add context
+ */
+Sentry.init({
+ // 📍 DSN - Data Source Name (identifies your Sentry project)
+ dsn: process.env.SENTRY_DSN,
+
+ // 🌍 Environment configuration
+ environment: process.env.SENTRY_ENVIRONMENT || "development",
+
+ // 📦 Release tracking
+ release: process.env.SENTRY_RELEASE,
+
+ // 📊 Sample Rates - Performance monitoring
+ // 100% in dev (test everything), 50% in production (balance visibility with costs)
+ tracesSampleRate: isProduction ? 0.5 : 1.0,
+ profilesSampleRate: isProduction ? 0.5 : 1.0,
+
+ // 🔌 Integrations
+ integrations: [
+ Sentry.extraErrorDataIntegration({
+ depth: 5, // Include up to 5 levels of nested objects
+ }),
+ ],
+
+ // 🎣 Filter expected errors - Don't send noise to Sentry
+ ignoreErrors: [
+ // NextAuth redirect errors - Expected behavior
+ "NEXT_REDIRECT",
+ "NEXT_NOT_FOUND",
+ // Expected HTTP errors - Expected when users lack permissions
+ "401", // Unauthorized
+ "403", // Forbidden
+ "404", // Not Found
+ ],
+
+ beforeSend(event, hint) {
+ // Add server context and tag errors appropriately
+ if (event.exception) {
+ const error = hint.originalException;
+
+ // Tag API errors for better filtering in Sentry dashboard
+ if (
+ error &&
+ typeof error === "object" &&
+ "message" in error &&
+ typeof error.message === "string"
+ ) {
+ if (error.message.includes("Server error")) {
+ event.tags = {
+ ...event.tags,
+ error_type: "server_error",
+ severity: "high",
+ };
+ } else if (error.message.includes("Request failed")) {
+ event.tags = {
+ ...event.tags,
+ error_type: "api_error",
+ };
+ }
+
+ // Don't send NextAuth expected errors
+ if (error.message.includes("NEXT_REDIRECT")) {
+ return null;
+ }
+ }
+ }
+
+ return event;
+ },
+});
diff --git a/ui/sentry/utils.ts b/ui/sentry/utils.ts
new file mode 100644
index 0000000000..d1dd49ea00
--- /dev/null
+++ b/ui/sentry/utils.ts
@@ -0,0 +1,36 @@
+/**
+ * Enum for standardized error types across the application
+ */
+export enum SentryErrorType {
+ // API Errors
+ API_ERROR = "api_error",
+ SERVER_ERROR = "server_error",
+ CLIENT_ERROR = "client_error",
+
+ // Request Processing
+ REQUEST_PROCESSING = "request_processing",
+ STREAM_PROCESSING = "stream_processing",
+
+ // Application Errors
+ APPLICATION_ERROR = "application_error",
+ UNEXPECTED_ERROR = "unexpected_error",
+ NON_ERROR_OBJECT = "non_error_object",
+
+ // Authentication
+ AUTH_ERROR = "auth_error",
+ PERMISSION_ERROR = "permission_error",
+
+ // Server Actions
+ SERVER_ACTION_ERROR = "server_action_error",
+}
+
+/**
+ * Enum for error sources
+ */
+export enum SentryErrorSource {
+ ERROR_BOUNDARY = "error_boundary",
+ API_ROUTE = "api_route",
+ SERVER_ACTION = "server_action",
+ HANDLE_API_ERROR = "handleApiError",
+ HANDLE_API_RESPONSE = "handleApiResponse",
+}
From 74025b2b5ef7e847d44b26ff9a74aed272255d3e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?=
Date: Tue, 11 Nov 2025 17:53:01 +0100
Subject: [PATCH 19/23] docs: add a architecture schema for MCP Server (#9214)
---
docs/getting-started/products/prowler-mcp.mdx | 11 ++++++++++-
docs/images/prowler_mcp_schema_dark.png | Bin 0 -> 336040 bytes
docs/images/prowler_mcp_schema_light.png | Bin 0 -> 339682 bytes
3 files changed, 10 insertions(+), 1 deletion(-)
create mode 100644 docs/images/prowler_mcp_schema_dark.png
create mode 100644 docs/images/prowler_mcp_schema_light.png
diff --git a/docs/getting-started/products/prowler-mcp.mdx b/docs/getting-started/products/prowler-mcp.mdx
index 40d627007c..94a5466edb 100644
--- a/docs/getting-started/products/prowler-mcp.mdx
+++ b/docs/getting-started/products/prowler-mcp.mdx
@@ -5,7 +5,7 @@ title: "Overview"
**Prowler MCP Server** brings the entire Prowler ecosystem to AI assistants through the Model Context Protocol (MCP). It enables seamless integration with AI tools like Claude Desktop, Cursor, and other MCP clients, allowing interaction with Prowler's security capabilities through natural language.
-**Preview Feature**: This MCP server is currently in preview and under active development. Features and functionality may change. We welcome your feedback—please report any issues on [GitHub](https://github.com/prowler-cloud/prowler/issues) or join our [Slack community](https://goto.prowler.com/slack) to discuss and share your thoughts.
+**Preview Feature**: This MCP server is currently under active development. Features and functionality may change. We welcome your feedback—please report any issues on [GitHub](https://github.com/prowler-cloud/prowler/issues) or join our [Slack community](https://goto.prowler.com/slack) to discuss and share your thoughts.
## What is the Model Context Protocol?
@@ -42,6 +42,15 @@ Search and retrieve official Prowler documentation:
- **Contextual Results**: Get relevant documentation pages with highlighted snippets.
- **Document Retrieval**: Access complete markdown content of any documentation file.
+## MCP Server Architecture
+
+The following diagram illustrates the Prowler MCP Server architecture and its integration points:
+
+
+
+
+The architecture shows how AI assistants connect through the MCP protocol to access Prowler's three main components: Prowler Cloud/App for security operations, Prowler Hub for security knowledge, and Prowler Documentation for guidance and reference.
+
## Use Cases
The Prowler MCP Server enables powerful workflows through AI assistants:
diff --git a/docs/images/prowler_mcp_schema_dark.png b/docs/images/prowler_mcp_schema_dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..7771557601c07e8356eb2de7a99f4e1b9c300c3c
GIT binary patch
literal 336040
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