Compare commits

..

3 Commits

Author SHA1 Message Date
Adrián Jesús Peña Rodríguez f5f4404ca9 chore: merge master 2026-02-26 19:13:58 +01:00
Adrián Jesús Peña Rodríguez 4dec30b4b6 fix(sdk): scope scan_id by provider and account
- Generate scan_id per provider account pair
- Adjust OCSF scan_id test to cover multiple accounts
2026-02-26 19:05:32 +01:00
Adrián Jesús Peña Rodríguez c0e5a7ce97 feat(ingestions): allow multiple scan_ids and providers inside the ocsf 2026-02-26 17:16:51 +01:00
23 changed files with 559 additions and 845 deletions
@@ -379,17 +379,6 @@ prowler_scan:
PROWLER_API_KEY: $PROWLER_API_KEY
```
## Billing impact
Each unique cloud account discovered in ingested OCSF findings counts as one **provider** in the Prowler Cloud subscription.
- **Existing providers**: If a cloud account was already connected as a provider, findings ingested for that account do **not** incur additional billing. The existing provider is reused.
- **New accounts**: Ingesting findings from accounts not yet connected to Prowler Cloud will result in new providers being created and counted toward the subscription.
- **High-volume ingestion**: Importing findings from many different cloud accounts will create a provider for each account. Review plan limits before large-scale ingestion.
- **Deleted providers**: Removing a provider no longer counts toward the subscription.
For pricing details, see [Prowler Cloud Pricing](https://prowler.com/pricing).
## Troubleshooting
### HTTP 401 Unauthorized
-5
View File
@@ -22,14 +22,9 @@ All notable changes to the **Prowler UI** are documented in this file.
- Attack Paths: Catches not found and permissions (for read only queries) errors [(#10140)](https://github.com/prowler-cloud/prowler/pull/10140)
- Provider connection flow was unified into a modal wizard with AWS Organizations bulk onboarding, safer secret retry handling, and more stable E2E coverage [(#10153)](https://github.com/prowler-cloud/prowler/pull/10153) [(#10154)](https://github.com/prowler-cloud/prowler/pull/10154) [(#10155)](https://github.com/prowler-cloud/prowler/pull/10155) [(#10156)](https://github.com/prowler-cloud/prowler/pull/10156) [(#10157)](https://github.com/prowler-cloud/prowler/pull/10157) [(#10158)](https://github.com/prowler-cloud/prowler/pull/10158)
### 🐞 Fixed
- Findings Severity Over Time chart on Overview not responding to provider and account filters, and chart clipping at Y-axis maximum values [(#10103)](https://github.com/prowler-cloud/prowler/pull/10103)
### 🔐 Security
- npm dependencies updated to resolve 11 Dependabot alerts (4 HIGH, 7 MEDIUM): fast-xml-parser, @modelcontextprotocol/sdk, tar, @isaacs/brace-expansion, hono, lodash, lodash-es [(#10052)](https://github.com/prowler-cloud/prowler/pull/10052)
- npm transitive dependencies patched to resolve 9 Dependabot alerts (2 CRITICAL, 3 HIGH, 2 MEDIUM, 2 LOW): fast-xml-parser, rollup, minimatch, ajv, hono, qs [(#10187)](https://github.com/prowler-cloud/prowler/pull/10187)
---
@@ -29,25 +29,6 @@ export const FindingSeverityOverTime = ({
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Sync data when SSR re-delivers filtered results (e.g. provider/account filter change).
// Uses the "set state during render" pattern so the update is synchronous — no flash of stale data.
const [prevInitialData, setPrevInitialData] = useState(initialData);
if (initialData !== prevInitialData) {
setPrevInitialData(initialData);
setData(initialData);
setError(null);
setTimeRange(DEFAULT_TIME_RANGE);
}
const getActiveProviderFilters = (): Record<string, string> => {
const filters: Record<string, string> = {};
const providerType = searchParams.get("filter[provider_type__in]");
const providerId = searchParams.get("filter[provider_id__in]");
if (providerType) filters["filter[provider_type__in]"] = providerType;
if (providerId) filters["filter[provider_id__in]"] = providerId;
return filters;
};
const handlePointClick = ({
point,
dataKey,
@@ -78,9 +59,14 @@ export const FindingSeverityOverTime = ({
}
// Preserve provider filters from overview
const providerFilters = getActiveProviderFilters();
for (const [key, value] of Object.entries(providerFilters)) {
params.set(key, value);
const providerType = searchParams.get("filter[provider_type__in]");
const providerId = searchParams.get("filter[provider_id__in]");
if (providerType) {
params.set("filter[provider_type__in]", providerType);
}
if (providerId) {
params.set("filter[provider_id__in]", providerId);
}
router.push(`/findings?${params.toString()}`);
@@ -94,7 +80,6 @@ export const FindingSeverityOverTime = ({
try {
const result = await getSeverityTrendsByTimeRange({
timeRange: newRange,
filters: getActiveProviderFilters(),
});
if (result.status === "success") {
+2 -3
View File
@@ -190,13 +190,13 @@ export function LineChart({
<div className="w-full">
<ChartContainer
config={chartConfig}
className="w-full"
className="w-full overflow-hidden"
style={{ height, aspectRatio: "auto" }}
>
<RechartsLine
data={data}
margin={{
top: 20,
top: 10,
left: 0,
right: 30,
bottom: 40,
@@ -222,7 +222,6 @@ export function LineChart({
tickLine={false}
axisLine={false}
tickMargin={8}
padding={{ top: 20 }}
tick={{
fill: "var(--color-text-neutral-secondary)",
fontSize: AXIS_FONT_SIZE,
@@ -123,7 +123,7 @@ export const SendInvitationForm = ({
onValueChange={field.onChange}
disabled={isSelectorDisabled}
>
<SelectTrigger aria-label="Select a role">
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
@@ -95,9 +95,6 @@ describe("useProviderWizardController", () => {
expect(result.current.wizardVariant).toBe("organizations");
expect(result.current.isProviderFlow).toBe(false);
expect(result.current.orgCurrentStep).toBe(ORG_WIZARD_STEP.SETUP);
expect(result.current.docsLink).toBe(
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
);
// When
act(() => {
@@ -3,7 +3,7 @@
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { DOCS_URLS, getProviderHelpText } from "@/lib/external-urls";
import { getProviderHelpText } from "@/lib/external-urls";
import { useOrgSetupStore } from "@/store/organizations/store";
import { useProviderWizardStore } from "@/store/provider-wizard/store";
import {
@@ -195,9 +195,9 @@ export function useProviderWizardController({
};
const isProviderFlow = wizardVariant === WIZARD_VARIANT.PROVIDER;
const docsLink = isProviderFlow
? getProviderHelpText(providerTypeHint ?? providerType ?? "").link
: DOCS_URLS.AWS_ORGANIZATIONS;
const docsLink = getProviderHelpText(
isProviderFlow ? (providerTypeHint ?? providerType ?? "") : "aws",
).link;
const resolvedFooterConfig: WizardFooterConfig =
isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.LAUNCH
? {
@@ -58,7 +58,7 @@ export function ProviderWizardModal({
initialData,
});
const scrollHintRefreshToken = `${wizardVariant}-${currentStep}-${orgCurrentStep}-${orgSetupPhase}`;
const { containerRef, sentinelRef, showScrollHint } = useScrollHint({
const { containerRef, showScrollHint, handleScroll } = useScrollHint({
enabled: open,
refreshToken: scrollHintRefreshToken,
});
@@ -106,6 +106,7 @@ export function ProviderWizardModal({
<div
ref={containerRef}
className="minimal-scrollbar h-full w-full overflow-y-scroll [scrollbar-gutter:stable] lg:ml-auto lg:max-w-[620px] xl:max-w-[700px]"
onScroll={handleScroll}
>
{isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.CONNECT && (
<ConnectStep
@@ -176,9 +177,6 @@ export function ProviderWizardModal({
onFooterChange={setFooterConfig}
/>
)}
{/* Sentinel element for IntersectionObserver scroll detection */}
<div ref={sentinelRef} aria-hidden className="h-px shrink-0" />
</div>
{showScrollHint && (
+35 -39
View File
@@ -1,67 +1,63 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { UIEvent, useEffect, useRef, useState } from "react";
interface UseScrollHintOptions {
enabled?: boolean;
refreshToken?: string | number;
}
/**
* Detects whether a scrollable container has overflow using an
* IntersectionObserver on a sentinel element placed at the end of the content.
*
* Uses callback refs (stored in state) so the observer is set up only after
* the DOM elements actually mount — critical for Radix Dialog portals where
* useRef would be null when the first useEffect fires.
*
* When the sentinel is NOT visible inside the container → content overflows
* and the user hasn't scrolled to the bottom → show hint.
*/
const SCROLL_THRESHOLD_PX = 4;
function shouldShowScrollHint(element: HTMLDivElement) {
const hasOverflow =
element.scrollHeight - element.clientHeight > SCROLL_THRESHOLD_PX;
const isAtBottom =
element.scrollTop + element.clientHeight >=
element.scrollHeight - SCROLL_THRESHOLD_PX;
return hasOverflow && !isAtBottom;
}
export function useScrollHint({
enabled = true,
refreshToken,
}: UseScrollHintOptions = {}) {
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
const [sentinelEl, setSentinelEl] = useState<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [showScrollHint, setShowScrollHint] = useState(false);
useEffect(() => {
if (!enabled || !containerEl || !sentinelEl) {
if (!enabled) {
setShowScrollHint(false);
return;
}
const observer = new IntersectionObserver(
([entry]) => {
setShowScrollHint(!entry.isIntersecting);
},
{
root: containerEl,
// Small margin so the hint hides slightly before the absolute bottom
rootMargin: "0px 0px 4px 0px",
threshold: 0,
},
);
const element = containerRef.current;
if (!element) return;
observer.observe(sentinelEl);
const recalculate = () => {
const el = containerRef.current;
if (!el) return;
setShowScrollHint(shouldShowScrollHint(el));
};
return () => observer.disconnect();
}, [enabled, refreshToken, containerEl, sentinelEl]);
const observer = new ResizeObserver(recalculate);
observer.observe(element);
// Stable callback refs — setState setters never change identity
const containerRef = useCallback(
(node: HTMLDivElement | null) => setContainerEl(node),
[],
);
const sentinelRef = useCallback(
(node: HTMLDivElement | null) => setSentinelEl(node),
[],
);
recalculate();
return () => {
observer.disconnect();
};
}, [enabled, refreshToken]);
const handleScroll = (event: UIEvent<HTMLDivElement>) => {
setShowScrollHint(shouldShowScrollHint(event.currentTarget));
};
return {
containerRef,
sentinelRef,
showScrollHint,
handleScroll,
};
}
-2
View File
@@ -4,8 +4,6 @@ import { IntegrationType } from "../types/integrations";
export const DOCS_URLS = {
FINDINGS_ANALYSIS:
"https://docs.prowler.com/user-guide/tutorials/prowler-app#step-8:-analyze-the-findings",
AWS_ORGANIZATIONS:
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
} as const;
export const getProviderHelpText = (provider: string) => {
+2 -8
View File
@@ -171,15 +171,9 @@
"@react-aria/interactions>react": "19.2.4",
"lodash": "4.17.23",
"lodash-es": "4.17.23",
"hono": "4.11.10",
"hono": "4.11.7",
"@isaacs/brace-expansion": "5.0.1",
"fast-xml-parser": "5.3.6",
"rollup@>=4": "4.59.0",
"minimatch@<4": "3.1.3",
"minimatch@>=9 <10": "9.0.6",
"ajv@<7": "6.14.0",
"ajv@>=8": "8.18.0",
"qs": "6.14.2"
"fast-xml-parser": "5.3.4"
}
},
"version": "0.0.1",
-7
View File
@@ -1,11 +1,4 @@
import { defineConfig, devices } from "@playwright/test";
import fs from "fs";
import path from "path";
const localEnvPath = path.resolve(__dirname, ".env.local");
if (fs.existsSync(localEnvPath)) {
process.loadEnvFile(localEnvPath);
}
export default defineConfig({
testDir: "./tests",
+181 -194
View File
@@ -15,15 +15,9 @@ overrides:
'@react-aria/interactions>react': 19.2.4
lodash: 4.17.23
lodash-es: 4.17.23
hono: 4.11.10
hono: 4.11.7
'@isaacs/brace-expansion': 5.0.1
fast-xml-parser: 5.3.6
rollup@>=4: 4.59.0
minimatch@<4: 3.1.3
minimatch@>=9 <10: 9.0.6
ajv@<7: 6.14.0
ajv@>=8: 8.18.0
qs: 6.14.2
fast-xml-parser: 5.3.4
importers:
@@ -1755,7 +1749,7 @@ packages:
resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
engines: {node: '>=18.14.1'}
peerDependencies:
hono: 4.11.10
hono: 4.11.7
'@hookform/resolvers@5.2.2':
resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
@@ -4013,7 +4007,7 @@ packages:
resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==}
engines: {node: '>=16.0.0 || 14 >= 14.17'}
peerDependencies:
rollup: 4.59.0
rollup: ^2.68.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
@@ -4022,133 +4016,133 @@ packages:
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: 4.59.0
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.59.0':
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
'@rollup/rollup-android-arm-eabi@4.55.2':
resolution: {integrity: sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.59.0':
resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==}
'@rollup/rollup-android-arm64@4.55.2':
resolution: {integrity: sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.59.0':
resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==}
'@rollup/rollup-darwin-arm64@4.55.2':
resolution: {integrity: sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.59.0':
resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==}
'@rollup/rollup-darwin-x64@4.55.2':
resolution: {integrity: sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.59.0':
resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==}
'@rollup/rollup-freebsd-arm64@4.55.2':
resolution: {integrity: sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.59.0':
resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==}
'@rollup/rollup-freebsd-x64@4.55.2':
resolution: {integrity: sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
'@rollup/rollup-linux-arm-gnueabihf@4.55.2':
resolution: {integrity: sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
'@rollup/rollup-linux-arm-musleabihf@4.55.2':
resolution: {integrity: sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.59.0':
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
'@rollup/rollup-linux-arm64-gnu@4.55.2':
resolution: {integrity: sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.59.0':
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
'@rollup/rollup-linux-arm64-musl@4.55.2':
resolution: {integrity: sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loong64-gnu@4.59.0':
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
'@rollup/rollup-linux-loong64-gnu@4.55.2':
resolution: {integrity: sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-loong64-musl@4.59.0':
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
'@rollup/rollup-linux-loong64-musl@4.55.2':
resolution: {integrity: sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
'@rollup/rollup-linux-ppc64-gnu@4.55.2':
resolution: {integrity: sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-ppc64-musl@4.59.0':
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
'@rollup/rollup-linux-ppc64-musl@4.55.2':
resolution: {integrity: sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
'@rollup/rollup-linux-riscv64-gnu@4.55.2':
resolution: {integrity: sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.59.0':
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
'@rollup/rollup-linux-riscv64-musl@4.55.2':
resolution: {integrity: sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.59.0':
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
'@rollup/rollup-linux-s390x-gnu@4.55.2':
resolution: {integrity: sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.59.0':
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
'@rollup/rollup-linux-x64-gnu@4.55.2':
resolution: {integrity: sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.59.0':
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
'@rollup/rollup-linux-x64-musl@4.55.2':
resolution: {integrity: sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==}
cpu: [x64]
os: [linux]
'@rollup/rollup-openbsd-x64@4.59.0':
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
'@rollup/rollup-openbsd-x64@4.55.2':
resolution: {integrity: sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.59.0':
resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==}
'@rollup/rollup-openharmony-arm64@4.55.2':
resolution: {integrity: sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.59.0':
resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==}
'@rollup/rollup-win32-arm64-msvc@4.55.2':
resolution: {integrity: sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.59.0':
resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==}
'@rollup/rollup-win32-ia32-msvc@4.55.2':
resolution: {integrity: sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.59.0':
resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==}
'@rollup/rollup-win32-x64-gnu@4.55.2':
resolution: {integrity: sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.59.0':
resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==}
'@rollup/rollup-win32-x64-msvc@4.55.2':
resolution: {integrity: sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==}
cpu: [x64]
os: [win32]
@@ -5203,7 +5197,7 @@ packages:
ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
ajv: 8.18.0
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
@@ -5211,7 +5205,7 @@ packages:
ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies:
ajv: 8.18.0
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
@@ -5219,13 +5213,13 @@ packages:
ajv-keywords@5.1.0:
resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==}
peerDependencies:
ajv: 8.18.0
ajv: ^8.8.2
ajv@6.14.0:
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
alert@6.0.2:
resolution: {integrity: sha512-Oi8u2HRNN6mzpjgKGii2Uuf9iOhyfbeUAHH/5MwnVmC8DS9GrEBjZBFpoavkNj+ZKnBr/Lqx+6YKLDKrggKfPA==}
@@ -5357,10 +5351,6 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -5385,9 +5375,8 @@ packages:
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
brace-expansion@5.0.3:
resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==}
engines: {node: 18 || 20 || >=22}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -6343,8 +6332,8 @@ packages:
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fast-xml-parser@5.3.6:
resolution: {integrity: sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==}
fast-xml-parser@5.3.4:
resolution: {integrity: sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==}
hasBin: true
fastq@1.20.1:
@@ -6645,8 +6634,8 @@ packages:
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
hono@4.11.10:
resolution: {integrity: sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==}
hono@4.11.7:
resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==}
engines: {node: '>=16.9.0'}
html-encoding-sniffer@6.0.0:
@@ -7547,11 +7536,11 @@ packages:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
minimatch@3.1.3:
resolution: {integrity: sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimatch@9.0.6:
resolution: {integrity: sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
minimist@1.2.8:
@@ -8111,8 +8100,8 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qs@6.14.2:
resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==}
qs@6.14.1:
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
engines: {node: '>=0.6'}
queue-microtask@1.2.3:
@@ -8373,8 +8362,8 @@ packages:
robust-predicates@3.0.2:
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
rollup@4.59.0:
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
rollup@4.55.2:
resolution: {integrity: sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
@@ -10238,13 +10227,13 @@ snapshots:
'@aws-sdk/xml-builder@3.969.0':
dependencies:
'@smithy/types': 4.12.0
fast-xml-parser: 5.3.6
fast-xml-parser: 5.3.4
tslib: 2.8.1
'@aws-sdk/xml-builder@3.972.4':
dependencies:
'@smithy/types': 4.12.0
fast-xml-parser: 5.3.6
fast-xml-parser: 5.3.4
tslib: 2.8.1
'@aws/lambda-invoke-store@0.2.3': {}
@@ -10624,7 +10613,7 @@ snapshots:
dependencies:
'@eslint/object-schema': 2.1.7
debug: 4.4.3
minimatch: 3.1.3
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -10638,14 +10627,14 @@ snapshots:
'@eslint/eslintrc@3.3.3':
dependencies:
ajv: 6.14.0
ajv: 6.12.6
debug: 4.4.3
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
import-fresh: 3.3.1
js-yaml: 4.1.1
minimatch: 3.1.3
minimatch: 3.1.2
strip-json-comments: 3.1.1
transitivePeerDependencies:
- supports-color
@@ -10667,7 +10656,7 @@ snapshots:
dependencies:
'@ndaidong/bellajs': 12.0.1
cross-fetch: 4.1.0
fast-xml-parser: 5.3.6
fast-xml-parser: 5.3.4
html-entities: 2.6.0
transitivePeerDependencies:
- encoding
@@ -11735,9 +11724,9 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@hono/node-server@1.19.9(hono@4.11.10)':
'@hono/node-server@1.19.9(hono@4.11.7)':
dependencies:
hono: 4.11.10
hono: 4.11.7
'@hookform/resolvers@5.2.2(react-hook-form@7.62.0(react@19.2.4))':
dependencies:
@@ -12108,9 +12097,9 @@ snapshots:
'@modelcontextprotocol/sdk@1.26.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)':
dependencies:
'@hono/node-server': 1.19.9(hono@4.11.10)
ajv: 8.18.0
ajv-formats: 3.0.1(ajv@8.18.0)
'@hono/node-server': 1.19.9(hono@4.11.7)
ajv: 8.17.1
ajv-formats: 3.0.1(ajv@8.17.1)
content-type: 1.0.5
cors: 2.8.5
cross-spawn: 7.0.6
@@ -12118,7 +12107,7 @@ snapshots:
eventsource-parser: 3.0.6
express: 5.2.1
express-rate-limit: 8.2.1(express@5.2.1)
hono: 4.11.10
hono: 4.11.7
jose: 6.1.3
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
@@ -12132,9 +12121,9 @@ snapshots:
'@modelcontextprotocol/sdk@1.26.0(@cfworker/json-schema@4.1.1)(zod@4.1.11)':
dependencies:
'@hono/node-server': 1.19.9(hono@4.11.10)
ajv: 8.18.0
ajv-formats: 3.0.1(ajv@8.18.0)
'@hono/node-server': 1.19.9(hono@4.11.7)
ajv: 8.17.1
ajv-formats: 3.0.1(ajv@8.17.1)
content-type: 1.0.5
cors: 2.8.5
cross-spawn: 7.0.6
@@ -12142,7 +12131,7 @@ snapshots:
eventsource-parser: 3.0.6
express: 5.2.1
express-rate-limit: 8.2.1(express@5.2.1)
hono: 4.11.10
hono: 4.11.7
jose: 6.1.3
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
@@ -14526,9 +14515,9 @@ snapshots:
'@rolldown/pluginutils@1.0.0-beta.53': {}
'@rollup/plugin-commonjs@28.0.1(rollup@4.59.0)':
'@rollup/plugin-commonjs@28.0.1(rollup@4.55.2)':
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.59.0)
'@rollup/pluginutils': 5.3.0(rollup@4.55.2)
commondir: 1.0.1
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.3)
@@ -14536,89 +14525,89 @@ snapshots:
magic-string: 0.30.21
picomatch: 4.0.3
optionalDependencies:
rollup: 4.59.0
rollup: 4.55.2
'@rollup/pluginutils@5.3.0(rollup@4.59.0)':
'@rollup/pluginutils@5.3.0(rollup@4.55.2)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
rollup: 4.59.0
rollup: 4.55.2
'@rollup/rollup-android-arm-eabi@4.59.0':
'@rollup/rollup-android-arm-eabi@4.55.2':
optional: true
'@rollup/rollup-android-arm64@4.59.0':
'@rollup/rollup-android-arm64@4.55.2':
optional: true
'@rollup/rollup-darwin-arm64@4.59.0':
'@rollup/rollup-darwin-arm64@4.55.2':
optional: true
'@rollup/rollup-darwin-x64@4.59.0':
'@rollup/rollup-darwin-x64@4.55.2':
optional: true
'@rollup/rollup-freebsd-arm64@4.59.0':
'@rollup/rollup-freebsd-arm64@4.55.2':
optional: true
'@rollup/rollup-freebsd-x64@4.59.0':
'@rollup/rollup-freebsd-x64@4.55.2':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
'@rollup/rollup-linux-arm-gnueabihf@4.55.2':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
'@rollup/rollup-linux-arm-musleabihf@4.55.2':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.59.0':
'@rollup/rollup-linux-arm64-gnu@4.55.2':
optional: true
'@rollup/rollup-linux-arm64-musl@4.59.0':
'@rollup/rollup-linux-arm64-musl@4.55.2':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.59.0':
'@rollup/rollup-linux-loong64-gnu@4.55.2':
optional: true
'@rollup/rollup-linux-loong64-musl@4.59.0':
'@rollup/rollup-linux-loong64-musl@4.55.2':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
'@rollup/rollup-linux-ppc64-gnu@4.55.2':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.59.0':
'@rollup/rollup-linux-ppc64-musl@4.55.2':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
'@rollup/rollup-linux-riscv64-gnu@4.55.2':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.59.0':
'@rollup/rollup-linux-riscv64-musl@4.55.2':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.59.0':
'@rollup/rollup-linux-s390x-gnu@4.55.2':
optional: true
'@rollup/rollup-linux-x64-gnu@4.59.0':
'@rollup/rollup-linux-x64-gnu@4.55.2':
optional: true
'@rollup/rollup-linux-x64-musl@4.59.0':
'@rollup/rollup-linux-x64-musl@4.55.2':
optional: true
'@rollup/rollup-openbsd-x64@4.59.0':
'@rollup/rollup-openbsd-x64@4.55.2':
optional: true
'@rollup/rollup-openharmony-arm64@4.59.0':
'@rollup/rollup-openharmony-arm64@4.55.2':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.59.0':
'@rollup/rollup-win32-arm64-msvc@4.55.2':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.59.0':
'@rollup/rollup-win32-ia32-msvc@4.55.2':
optional: true
'@rollup/rollup-win32-x64-gnu@4.59.0':
'@rollup/rollup-win32-x64-gnu@4.55.2':
optional: true
'@rollup/rollup-win32-x64-msvc@4.59.0':
'@rollup/rollup-win32-x64-msvc@4.55.2':
optional: true
'@rtsao/scc@1.1.0': {}
@@ -14717,7 +14706,7 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.39.0
'@rollup/plugin-commonjs': 28.0.1(rollup@4.59.0)
'@rollup/plugin-commonjs': 28.0.1(rollup@4.55.2)
'@sentry-internal/browser-utils': 10.27.0
'@sentry/bundler-plugin-core': 4.7.0
'@sentry/core': 10.27.0
@@ -14728,7 +14717,7 @@ snapshots:
'@sentry/webpack-plugin': 4.7.0(webpack@5.104.1)
next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
resolve: 1.22.8
rollup: 4.59.0
rollup: 4.55.2
stacktrace-parser: 0.1.11
transitivePeerDependencies:
- '@opentelemetry/context-async-hooks'
@@ -14791,7 +14780,7 @@ snapshots:
'@sentry/node-core': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0)
'@sentry/opentelemetry': 10.27.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0)
import-in-the-middle: 2.0.0
minimatch: 9.0.6
minimatch: 9.0.5
transitivePeerDependencies:
- supports-color
@@ -15710,7 +15699,7 @@ snapshots:
'@typescript-eslint/types': 8.53.0
'@typescript-eslint/visitor-keys': 8.53.0
debug: 4.4.3
minimatch: 9.0.6
minimatch: 9.0.5
semver: 7.7.3
tinyglobby: 0.2.15
ts-api-utils: 2.4.0(typescript@5.5.4)
@@ -15978,27 +15967,27 @@ snapshots:
'@opentelemetry/api': 1.9.0
zod: 4.1.11
ajv-formats@2.1.1(ajv@8.18.0):
ajv-formats@2.1.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.18.0
ajv: 8.17.1
ajv-formats@3.0.1(ajv@8.18.0):
ajv-formats@3.0.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.18.0
ajv: 8.17.1
ajv-keywords@5.1.0(ajv@8.18.0):
ajv-keywords@5.1.0(ajv@8.17.1):
dependencies:
ajv: 8.18.0
ajv: 8.17.1
fast-deep-equal: 3.1.3
ajv@6.14.0:
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
fast-json-stable-stringify: 2.1.0
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ajv@8.18.0:
ajv@8.17.1:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.0
@@ -16154,8 +16143,6 @@ snapshots:
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
base64-js@1.5.1: {}
baseline-browser-mapping@2.9.15: {}
@@ -16174,7 +16161,7 @@ snapshots:
http-errors: 2.0.1
iconv-lite: 0.7.2
on-finished: 2.4.1
qs: 6.14.2
qs: 6.14.1
raw-body: 3.0.2
type-is: 2.0.1
transitivePeerDependencies:
@@ -16187,9 +16174,9 @@ snapshots:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@5.0.3:
brace-expansion@2.0.2:
dependencies:
balanced-match: 4.0.4
balanced-match: 1.0.2
braces@3.0.3:
dependencies:
@@ -17009,7 +16996,7 @@ snapshots:
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
minimatch: 3.1.3
minimatch: 3.1.2
object.fromentries: 2.0.8
object.groupby: 1.0.3
object.values: 1.2.1
@@ -17037,7 +17024,7 @@ snapshots:
hasown: 2.0.2
jsx-ast-utils: 3.3.5
language-tags: 1.0.9
minimatch: 3.1.3
minimatch: 3.1.2
object.fromentries: 2.0.8
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
@@ -17048,7 +17035,7 @@ snapshots:
eslint-plugin-es: 3.0.1(eslint@9.39.2(jiti@2.6.1))
eslint-utils: 2.1.0
ignore: 5.3.2
minimatch: 3.1.3
minimatch: 3.1.2
resolve: 1.22.11
semver: 6.3.1
@@ -17085,7 +17072,7 @@ snapshots:
estraverse: 5.3.0
hasown: 2.0.2
jsx-ast-utils: 3.3.5
minimatch: 3.1.3
minimatch: 3.1.2
object.entries: 1.1.9
object.fromentries: 2.0.8
object.values: 1.2.1
@@ -17143,7 +17130,7 @@ snapshots:
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
'@types/estree': 1.0.8
ajv: 6.14.0
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.3
@@ -17162,7 +17149,7 @@ snapshots:
is-glob: 4.0.3
json-stable-stringify-without-jsonify: 1.0.1
lodash.merge: 4.6.2
minimatch: 3.1.3
minimatch: 3.1.2
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
@@ -17282,7 +17269,7 @@ snapshots:
once: 1.4.0
parseurl: 1.3.3
proxy-addr: 2.0.7
qs: 6.14.2
qs: 6.14.1
range-parser: 1.2.1
router: 2.2.0
send: 1.2.1
@@ -17326,7 +17313,7 @@ snapshots:
fast-uri@3.1.0: {}
fast-xml-parser@5.3.6:
fast-xml-parser@5.3.4:
dependencies:
strnum: 2.1.2
@@ -17502,7 +17489,7 @@ snapshots:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.6
minimatch: 9.0.5
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
@@ -17686,7 +17673,7 @@ snapshots:
dependencies:
react-is: 16.13.1
hono@4.11.10: {}
hono@4.11.7: {}
html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
dependencies:
@@ -18784,13 +18771,13 @@ snapshots:
dependencies:
'@isaacs/brace-expansion': 5.0.1
minimatch@3.1.3:
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
minimatch@9.0.6:
minimatch@9.0.5:
dependencies:
brace-expansion: 5.0.3
brace-expansion: 2.0.2
minimist@1.2.8: {}
@@ -19272,7 +19259,7 @@ snapshots:
punycode@2.3.1: {}
qs@6.14.2:
qs@6.14.1:
dependencies:
side-channel: 1.1.0
@@ -19650,35 +19637,35 @@ snapshots:
robust-predicates@3.0.2: {}
rollup@4.59.0:
rollup@4.55.2:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.59.0
'@rollup/rollup-android-arm64': 4.59.0
'@rollup/rollup-darwin-arm64': 4.59.0
'@rollup/rollup-darwin-x64': 4.59.0
'@rollup/rollup-freebsd-arm64': 4.59.0
'@rollup/rollup-freebsd-x64': 4.59.0
'@rollup/rollup-linux-arm-gnueabihf': 4.59.0
'@rollup/rollup-linux-arm-musleabihf': 4.59.0
'@rollup/rollup-linux-arm64-gnu': 4.59.0
'@rollup/rollup-linux-arm64-musl': 4.59.0
'@rollup/rollup-linux-loong64-gnu': 4.59.0
'@rollup/rollup-linux-loong64-musl': 4.59.0
'@rollup/rollup-linux-ppc64-gnu': 4.59.0
'@rollup/rollup-linux-ppc64-musl': 4.59.0
'@rollup/rollup-linux-riscv64-gnu': 4.59.0
'@rollup/rollup-linux-riscv64-musl': 4.59.0
'@rollup/rollup-linux-s390x-gnu': 4.59.0
'@rollup/rollup-linux-x64-gnu': 4.59.0
'@rollup/rollup-linux-x64-musl': 4.59.0
'@rollup/rollup-openbsd-x64': 4.59.0
'@rollup/rollup-openharmony-arm64': 4.59.0
'@rollup/rollup-win32-arm64-msvc': 4.59.0
'@rollup/rollup-win32-ia32-msvc': 4.59.0
'@rollup/rollup-win32-x64-gnu': 4.59.0
'@rollup/rollup-win32-x64-msvc': 4.59.0
'@rollup/rollup-android-arm-eabi': 4.55.2
'@rollup/rollup-android-arm64': 4.55.2
'@rollup/rollup-darwin-arm64': 4.55.2
'@rollup/rollup-darwin-x64': 4.55.2
'@rollup/rollup-freebsd-arm64': 4.55.2
'@rollup/rollup-freebsd-x64': 4.55.2
'@rollup/rollup-linux-arm-gnueabihf': 4.55.2
'@rollup/rollup-linux-arm-musleabihf': 4.55.2
'@rollup/rollup-linux-arm64-gnu': 4.55.2
'@rollup/rollup-linux-arm64-musl': 4.55.2
'@rollup/rollup-linux-loong64-gnu': 4.55.2
'@rollup/rollup-linux-loong64-musl': 4.55.2
'@rollup/rollup-linux-ppc64-gnu': 4.55.2
'@rollup/rollup-linux-ppc64-musl': 4.55.2
'@rollup/rollup-linux-riscv64-gnu': 4.55.2
'@rollup/rollup-linux-riscv64-musl': 4.55.2
'@rollup/rollup-linux-s390x-gnu': 4.55.2
'@rollup/rollup-linux-x64-gnu': 4.55.2
'@rollup/rollup-linux-x64-musl': 4.55.2
'@rollup/rollup-openbsd-x64': 4.55.2
'@rollup/rollup-openharmony-arm64': 4.55.2
'@rollup/rollup-win32-arm64-msvc': 4.55.2
'@rollup/rollup-win32-ia32-msvc': 4.55.2
'@rollup/rollup-win32-x64-gnu': 4.55.2
'@rollup/rollup-win32-x64-msvc': 4.55.2
fsevents: 2.3.3
roughjs@4.6.6:
@@ -19742,9 +19729,9 @@ snapshots:
schema-utils@4.3.3:
dependencies:
'@types/json-schema': 7.0.15
ajv: 8.18.0
ajv-formats: 2.1.1(ajv@8.18.0)
ajv-keywords: 5.1.0(ajv@8.18.0)
ajv: 8.17.1
ajv-formats: 2.1.1(ajv@8.17.1)
ajv-keywords: 5.1.0(ajv@8.17.1)
scroll-into-view-if-needed@3.0.10:
dependencies:
@@ -20571,7 +20558,7 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.59.0
rollup: 4.55.2
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.10.8
+10 -31
View File
@@ -1,7 +1,8 @@
import { expect, test } from "@playwright/test";
import { TEST_CREDENTIALS } from "../helpers";
import { getSessionWithoutCookies, TEST_CREDENTIALS } from "../helpers";
import { ProvidersPage } from "../providers/providers-page";
import { ScansPage } from "../scans/scans-page";
import { SignInPage } from "../sign-in-base/sign-in-base-page";
import { SignUpPage } from "../sign-up/sign-up-page";
@@ -29,46 +30,24 @@ test.describe("Middleware Error Handling", () => {
test(
"should maintain protection after session error",
{ tag: ["@e2e", "@auth", "@middleware", "@AUTH-MW-E2E-002"] },
async ({ page, context, browser }) => {
async ({ page, context }) => {
const signInPage = new SignInPage(page);
const providersPage = new ProvidersPage(page);
const scansPage = new ScansPage(page);
await signInPage.loginAndVerify(TEST_CREDENTIALS.VALID);
await providersPage.goto();
await providersPage.verifyPageLoaded();
// Build an isolated context with an explicitly invalid auth token.
// This avoids races from active tabs rehydrating cookies in the original context.
const authenticatedState = await context.storageState();
const authCookies = authenticatedState.cookies.filter((cookie) =>
/(authjs|next-auth)/i.test(cookie.name),
);
expect(authCookies.length).toBeGreaterThan(0);
// Remove auth cookies to simulate a broken/expired session deterministically.
await context.clearCookies();
const invalidSessionContext = await browser.newContext({
storageState: {
origins: authenticatedState.origins,
cookies: authenticatedState.cookies.map((cookie) =>
/(authjs|next-auth)/i.test(cookie.name)
? { ...cookie, value: "invalid.session.token" }
: cookie,
),
},
});
const expiredSession = await getSessionWithoutCookies(page);
expect(expiredSession).toBeNull();
try {
// Use a fresh page to force a full navigation through proxy in Next.js 16.
const freshPage = await invalidSessionContext.newPage();
const freshSignInPage = new SignInPage(freshPage);
const cacheBuster = Date.now();
await freshPage.goto(`/scans?e2e_mw=${cacheBuster}`, {
waitUntil: "commit",
});
await freshSignInPage.verifyRedirectWithCallback("/scans");
} finally {
await invalidSessionContext.close();
}
await scansPage.goto();
await signInPage.verifyOnSignInPage();
},
);
+45 -10
View File
@@ -135,30 +135,47 @@ export async function deleteProviderIfExists(page: ProvidersPage, providerUID: s
await page.goto();
await expect(page.providersTable).toBeVisible({ timeout: 10000 });
// Find and use the search input to filter the table
const searchInput = page.page.getByPlaceholder(/search|filter/i);
await expect(searchInput).toBeVisible({ timeout: 5000 });
// Clear and search for the specific provider
await searchInput.clear();
await searchInput.fill(providerUID);
await searchInput.press("Enter");
// Additional wait for React table to re-render with the server-filtered data
// The filtering happens on the server, but the table component needs time
// to process the response and update the DOM after network idle
await page.page.waitForTimeout(1500);
// Get all rows from the table
const allRows = page.providersTable.locator("tbody tr");
// Helper function to check if a row is the "No results" row
const isNoResultsRow = async (row: Locator): Promise<boolean> => {
const text = await row.textContent();
return text?.includes("No results") || text?.includes("No data") || false;
};
// Helper function to find the row with the specific UID
const findProviderRow = async (): Promise<Locator | null> => {
const rowByText = page.providersTable
.locator("tbody tr")
.filter({ hasText: providerUID })
.first();
if (await rowByText.isVisible().catch(() => false)) {
return rowByText;
}
const count = await allRows.count();
for (let i = 0; i < count; i++) {
const row = allRows.nth(i);
// Skip "No results" rows
if (await isNoResultsRow(row)) {
continue;
}
const rowText = await row.textContent();
if (rowText?.includes(providerUID)) {
// Check if this row contains the UID in the UID column (column 3)
const uidCell = row.locator("td").nth(3);
const uidText = await uidCell.textContent();
if (uidText?.includes(providerUID)) {
return row;
}
}
@@ -166,6 +183,24 @@ export async function deleteProviderIfExists(page: ProvidersPage, providerUID: s
return null;
};
// Wait for filtering to complete (max 0 or 1 data rows)
await expect(async () => {
await findProviderRow();
const count = await allRows.count();
// Count only real data rows (not "No results")
let dataRowCount = 0;
for (let i = 0; i < count; i++) {
if (!(await isNoResultsRow(allRows.nth(i)))) {
dataRowCount++;
}
}
// Should have 0 or 1 data row
expect(dataRowCount).toBeLessThanOrEqual(1);
}).toPass({ timeout: 20000 });
// Find the provider row
const targetRow = await findProviderRow();
+16 -7
View File
@@ -40,10 +40,11 @@ export class InvitationsPage extends BasePage {
// Form inputs
this.emailInput = page.getByRole("textbox", { name: "Email" });
// Form select (Radix Select renders <button role="combobox"> with aria-label)
this.roleSelect = page.getByRole("combobox", {
name: /Select a role/i,
});
// Form select
this.roleSelect = page
.getByRole("combobox", { name: /Role|Select a role/i })
.or(page.getByRole("button", { name: /Role|Select a role/i }))
.first();
// Form details
this.reviewInvitationDetailsButton = page.getByRole("button", {
@@ -95,15 +96,23 @@ export class InvitationsPage extends BasePage {
}
async selectRole(role: string): Promise<void> {
// Select the role option
// Open the role dropdown
await expect(this.roleSelect).toBeVisible({ timeout: 15000 });
await this.roleSelect.click();
// Prefer ARIA role option inside listbox
const option = this.page.getByRole("option", {
name: new RegExp(role, "i"),
name: new RegExp(`^${role}$`, "i"),
});
await expect(option.first()).toBeVisible({ timeout: 10000 });
await option.first().click();
if (await option.count()) {
await option.first().click();
} else {
throw new Error(`Role option ${role} not found`);
}
// Ensure a role value was selected in the trigger
await expect(this.roleSelect).not.toContainText(/Select a role/i);
}
+31 -36
View File
@@ -5,80 +5,69 @@ import { SignUpPage } from "../sign-up/sign-up-page";
import { SignInPage } from "../sign-in-base/sign-in-base-page";
import { UserProfilePage } from "../profile/profile-page";
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
test.describe("New user invitation", () => {
// Invitations page object
let invitationsPage: InvitationsPage;
// Setup before each test
test.beforeEach(async ({ page }) => {
invitationsPage = new InvitationsPage(page);
});
// Use admin authentication for invitations management
test.use({ storageState: "playwright/.auth/admin_user.json" });
test(
"should send an invitation successfully",
"should invite a new user",
{
tag: ["@critical", "@e2e", "@invitations", "@INVITATION-E2E-001"],
},
async () => {
const suffix = makeSuffix(10);
const uniqueEmail = `e2e+${suffix}@prowler.com`;
await invitationsPage.goto();
await invitationsPage.verifyPageLoaded();
await invitationsPage.clickInviteButton();
await invitationsPage.verifyInvitePageLoaded();
await invitationsPage.fillEmail(uniqueEmail);
await invitationsPage.selectRole("admin");
await invitationsPage.clickSendInviteButton();
await invitationsPage.verifyInviteDataPageLoaded();
},
);
test(
"should invite a new user and verify signup and login",
{
tag: ["@critical", "@e2e", "@invitations", "@INVITATION-E2E-002"],
},
async ({ browser }) => {
test.skip(!isCloudEnv, "Requires email-verification flow (Cloud only)");
async ({ page, browser }) => {
// Test data from environment variables
const password = process.env.E2E_NEW_USER_PASSWORD;
const organizationId = process.env.E2E_ORGANIZATION_ID;
// Validate required environment variables
if (!password || !organizationId) {
throw new Error(
"E2E_NEW_USER_PASSWORD or E2E_ORGANIZATION_ID environment variable is not set",
);
}
// Generate unique test data
const suffix = makeSuffix(10);
const uniqueEmail = `e2e+${suffix}@prowler.com`;
// Navigate to providers page
await invitationsPage.goto();
await invitationsPage.verifyPageLoaded();
// Press the invite button
await invitationsPage.clickInviteButton();
await invitationsPage.verifyInvitePageLoaded();
// Fill the email
await invitationsPage.fillEmail(uniqueEmail);
await invitationsPage.selectRole("admin");
// Select the role option
await invitationsPage.selectRole("e2e_admin");
// Press the send invitation button
await invitationsPage.clickSendInviteButton();
await invitationsPage.verifyInviteDataPageLoaded();
// Get the share url
const shareUrl = await invitationsPage.getShareUrl();
const inviteContext = await browser.newContext({
storageState: { cookies: [], origins: [] },
});
// Navigate to the share url with a new context to avoid cookies from the admin context
const inviteContext = await browser.newContext({ storageState: { cookies: [], origins: [] } });
const signUpPage = new SignUpPage(await inviteContext.newPage());
// Navigate to the share url
await signUpPage.gotoInvite(shareUrl);
// Fill and submit the sign-up form
await signUpPage.signup({
name: `E2E User ${suffix}`,
email: uniqueEmail,
@@ -87,9 +76,13 @@ test.describe("New user invitation", () => {
acceptTerms: true,
});
// Verify no errors occurred during sign-up
await signUpPage.verifyNoErrors();
// Verify redirect to login page (OSS environment)
await signUpPage.verifyRedirectToLogin();
// Verify the newly created user can log in successfully with the new context
const signInPage = new SignInPage(await inviteContext.newPage());
await signInPage.goto();
await signInPage.login({
@@ -97,13 +90,15 @@ test.describe("New user invitation", () => {
password: password,
});
await signInPage.verifySuccessfulLogin();
const userProfilePage = new UserProfilePage(
await inviteContext.newPage(),
);
// Navigate to the user profile page
const userProfilePage = new UserProfilePage(await inviteContext.newPage());
await userProfilePage.goto();
// Verify if user is added to the organization
await userProfilePage.verifyOrganizationId(organizationId);
// Close the invite context
await inviteContext.close();
},
);
+71 -171
View File
@@ -7,16 +7,6 @@ export interface AWSProviderData {
alias?: string;
}
export interface AWSOrganizationsProviderData {
organizationId: string;
organizationName?: string;
}
export interface AWSOrganizationsProviderCredential {
roleArn: string;
stackSetDeployed?: boolean;
}
// AZURE provider data
export interface AZUREProviderData {
subscriptionId: string;
@@ -502,9 +492,13 @@ export class ProvidersPage extends BasePage {
this.roleArnInput = page.getByRole("textbox", { name: "Role ARN" });
this.externalIdInput = page.getByRole("textbox", { name: "External ID" });
// Inputs for static credentials (type="password" fields have no textbox role)
this.accessKeyIdInput = page.getByLabel(/Access Key ID/i).first();
this.secretAccessKeyInput = page.getByLabel(/Secret Access Key/i).first();
// Inputs for static credentials
this.accessKeyIdInput = page.getByRole("textbox", {
name: "Access Key ID",
});
this.secretAccessKeyInput = page.getByRole("textbox", {
name: "Secret Access Key",
});
// Delete button in confirmation modal
this.deleteProviderConfirmationButton = page.getByRole("button", {
@@ -584,17 +578,17 @@ export class ProvidersPage extends BasePage {
}
async selectAWSSingleAccountMethod(): Promise<void> {
const singleAccountOption = this.page.getByRole("radio", {
name: "Add A Single AWS Cloud Account",
exact: true,
});
await expect(singleAccountOption).toBeVisible({ timeout: 10000 });
await singleAccountOption.click();
await this.page
.getByRole("button", {
name: "Add A Single AWS Cloud Account",
exact: true,
})
.click();
}
async selectAWSOrganizationsMethod(): Promise<void> {
await this.page
.getByRole("radio", {
.getByRole("button", {
name: "Add Multiple Accounts With AWS Organizations",
exact: true,
})
@@ -639,8 +633,16 @@ export class ProvidersPage extends BasePage {
}
async fillAWSProviderDetails(data: AWSProviderData): Promise<void> {
await this.selectAWSSingleAccountMethod();
await expect(this.accountIdInput).toBeVisible({ timeout: 10000 });
// Fill the AWS provider details
const singleAccountButton = this.page.getByRole("button", {
name: "Add A Single AWS Cloud Account",
exact: true,
});
if (await singleAccountButton.isVisible().catch(() => false)) {
await singleAccountButton.click();
}
await this.accountIdInput.fill(data.accountId);
if (data.alias) {
@@ -648,43 +650,6 @@ export class ProvidersPage extends BasePage {
}
}
async fillAWSOrganizationsProviderDetails(
data: AWSOrganizationsProviderData,
): Promise<void> {
const organizationIdInput = this.page.getByRole("textbox", {
name: "Organization ID",
exact: true,
});
await expect(organizationIdInput).toBeVisible({ timeout: 10000 });
await organizationIdInput.fill(data.organizationId.toLowerCase());
if (data.organizationName) {
await this.page
.getByRole("textbox", { name: "Name (optional)", exact: true })
.fill(data.organizationName);
}
}
async fillAWSOrganizationsCredentials(
credentials: AWSOrganizationsProviderCredential,
): Promise<void> {
const roleArnInput = this.page.getByRole("textbox", {
name: "Role ARN",
exact: true,
});
await expect(roleArnInput).toBeVisible({ timeout: 10000 });
await roleArnInput.fill(credentials.roleArn);
if (credentials.stackSetDeployed ?? true) {
const stackSetCheckbox = this.page.getByRole("checkbox", {
name: /The StackSet has been successfully deployed in AWS/i,
});
if (!(await stackSetCheckbox.isChecked())) {
await stackSetCheckbox.click();
}
}
}
async fillAZUREProviderDetails(data: AZUREProviderData): Promise<void> {
// Fill the AWS provider details
@@ -740,13 +705,22 @@ export class ProvidersPage extends BasePage {
async clickNext(): Promise<void> {
await this.verifyWizardModalOpen();
const launchScanButton = this.page.getByRole("button", {
name: "Launch scan",
exact: true,
});
if (await launchScanButton.isVisible().catch(() => false)) {
await launchScanButton.click();
await this.handleLaunchScanCompletion();
return;
}
const actionNames = [
"Go to scans",
"Authenticate",
"Next",
"Save",
"Check connection",
"Launch scan",
] as const;
for (const actionName of actionNames) {
@@ -756,9 +730,6 @@ export class ProvidersPage extends BasePage {
});
if (await button.isVisible().catch(() => false)) {
await button.click();
if (actionName === "Launch scan") {
await this.handleLaunchScanCompletion();
}
return;
}
}
@@ -769,36 +740,38 @@ export class ProvidersPage extends BasePage {
}
private async handleLaunchScanCompletion(): Promise<void> {
const errorMessage = this.page
.locator(
"div.border-border-error, div.bg-red-100, p.text-text-error-primary, p.text-danger",
)
.first();
const goToScansButton = this.page.getByRole("button", {
name: "Go to scans",
exact: true,
});
const connectionError = this.page.locator(
"div.border-border-error p.text-text-error-primary",
);
try {
await Promise.race([
this.page.waitForURL(/\/scans/, { timeout: 20000 }),
goToScansButton.waitFor({ state: "visible", timeout: 20000 }),
connectionError.waitFor({ state: "visible", timeout: 20000 }),
this.page.waitForURL(/\/scans/, { timeout: 30000 }),
goToScansButton.waitFor({ state: "visible", timeout: 30000 }),
errorMessage.waitFor({ state: "visible", timeout: 30000 }),
]);
} catch {
// Continue and inspect visible state below.
}
if (await connectionError.isVisible().catch(() => false)) {
const errorText = await connectionError.textContent();
const isErrorVisible = await errorMessage.isVisible().catch(() => false);
if (isErrorVisible) {
const errorText = await errorMessage.textContent();
throw new Error(
`Test connection failed with error: ${errorText?.trim() || "Unknown error"}`,
);
}
if (this.page.url().includes("/scans")) {
return;
}
if (await goToScansButton.isVisible().catch(() => false)) {
const isGoToScansVisible = await goToScansButton
.isVisible()
.catch(() => false);
if (isGoToScansVisible) {
await goToScansButton.click();
await this.page.waitForURL(/\/scans/, { timeout: 30000 });
}
@@ -854,48 +827,19 @@ export class ProvidersPage extends BasePage {
}
async fillRoleCredentials(credentials: AWSProviderCredential): Promise<void> {
await expect(this.roleArnInput).toBeVisible({ timeout: 10000 });
const accessKeyInputInWizard = this.wizardModal.getByPlaceholder(
"Enter the AWS Access Key ID",
);
const secretKeyInputInWizard = this.wizardModal.getByPlaceholder(
"Enter the AWS Secret Access Key",
);
const accessKeyId =
credentials.accessKeyId || process.env.E2E_AWS_PROVIDER_ACCESS_KEY;
const secretAccessKey =
credentials.secretAccessKey || process.env.E2E_AWS_PROVIDER_SECRET_KEY;
// Fill the role credentials form
const shouldFillStaticKeys = Boolean(
accessKeyId || secretAccessKey,
);
if (shouldFillStaticKeys) {
const accessKeyIsVisible = await accessKeyInputInWizard
.isVisible()
.catch(() => false);
// In cloud env the default can be SDK mode, so expose Access/Secret explicitly.
if (!accessKeyIsVisible) {
await this.selectAuthenticationMethod(
AWS_CREDENTIAL_OPTIONS.AWS_ROLE_ARN,
);
}
if (credentials.accessKeyId) {
await this.accessKeyIdInput.fill(credentials.accessKeyId);
}
if (accessKeyId) {
await expect(accessKeyInputInWizard).toBeVisible({ timeout: 10000 });
await accessKeyInputInWizard.fill(accessKeyId);
await expect(accessKeyInputInWizard).toHaveValue(accessKeyId);
}
if (secretAccessKey) {
await expect(secretKeyInputInWizard).toBeVisible({ timeout: 10000 });
await secretKeyInputInWizard.fill(secretAccessKey);
await expect(secretKeyInputInWizard).toHaveValue(secretAccessKey);
if (credentials.secretAccessKey) {
await this.secretAccessKeyInput.fill(credentials.secretAccessKey);
}
if (credentials.roleArn) {
await this.roleArnInput.fill(credentials.roleArn);
}
if (credentials.externalId) {
// External ID may be prefilled and disabled; only fill if enabled
if (await this.externalIdInput.isEnabled()) {
await this.externalIdInput.fill(credentials.externalId);
}
@@ -905,26 +849,13 @@ export class ProvidersPage extends BasePage {
async fillStaticCredentials(
credentials: AWSProviderCredential,
): Promise<void> {
const accessKeyInputInWizard = this.wizardModal.getByPlaceholder(
"Enter the AWS Access Key ID",
);
const secretKeyInputInWizard = this.wizardModal.getByPlaceholder(
"Enter the AWS Secret Access Key",
);
const accessKeyId =
credentials.accessKeyId || process.env.E2E_AWS_PROVIDER_ACCESS_KEY;
const secretAccessKey =
credentials.secretAccessKey || process.env.E2E_AWS_PROVIDER_SECRET_KEY;
// Fill the static credentials form
if (accessKeyId) {
await expect(accessKeyInputInWizard).toBeVisible({ timeout: 10000 });
await accessKeyInputInWizard.fill(accessKeyId);
await expect(accessKeyInputInWizard).toHaveValue(accessKeyId);
if (credentials.accessKeyId) {
await this.accessKeyIdInput.fill(credentials.accessKeyId);
}
if (secretAccessKey) {
await expect(secretKeyInputInWizard).toBeVisible({ timeout: 10000 });
await secretKeyInputInWizard.fill(secretAccessKey);
await expect(secretKeyInputInWizard).toHaveValue(secretAccessKey);
if (credentials.secretAccessKey) {
await this.secretAccessKeyInput.fill(credentials.secretAccessKey);
}
}
@@ -1195,22 +1126,11 @@ export class ProvidersPage extends BasePage {
}
async verifyCredentialsPageLoaded(): Promise<void> {
// Verify the credentials page is loaded
await this.verifyPageHasProwlerTitle();
await this.verifyWizardModalOpen();
const selectorRadio = this.wizardModal.getByRole("radio", {
name: /Connect assuming IAM Role/i,
});
const selectorHint = this.wizardModal.getByText(/Using IAM Role/i);
const roleArnInForm = this.wizardModal.getByRole("textbox", {
name: "Role ARN",
});
await Promise.race([
selectorRadio.waitFor({ state: "visible", timeout: 20000 }),
selectorHint.waitFor({ state: "visible", timeout: 20000 }),
roleArnInForm.waitFor({ state: "visible", timeout: 20000 }),
]);
await expect(this.roleCredentialsRadio).toBeVisible();
}
async verifyM365CredentialsPageLoaded(): Promise<void> {
@@ -1266,17 +1186,12 @@ export class ProvidersPage extends BasePage {
await this.verifyPageHasProwlerTitle();
await this.verifyWizardModalOpen();
// Some providers show "Check connection" before "Launch scan".
const launchAction = this.page
.getByRole("button", { name: "Launch scan", exact: true })
.or(
this.page.getByRole("button", {
name: "Check connection",
exact: true,
}),
);
// Verify the Launch scan button is visible
const launchScanButton = this.page
.locator("button")
.filter({ hasText: "Launch scan" });
await expect(launchAction).toBeVisible();
await expect(launchScanButton).toBeVisible();
}
async verifyLoadProviderPageAfterNewProvider(): Promise<void> {
@@ -1320,9 +1235,7 @@ export class ProvidersPage extends BasePage {
.click({ force: true });
} else if (method === AWS_CREDENTIAL_OPTIONS.AWS_SDK_DEFAULT) {
await this.page
.getByRole("option", {
name: /AWS SDK Default|Prowler Cloud will assume your IAM role/i,
})
.getByRole("option", { name: "AWS SDK Default" })
.click({ force: true });
} else {
throw new Error(`Invalid authentication method: ${method}`);
@@ -1365,7 +1278,9 @@ export class ProvidersPage extends BasePage {
}
async verifyTestConnectionPageLoaded(): Promise<void> {
// Verify the test connection page is loaded
await this.verifyPageHasProwlerTitle();
await this.verifyWizardModalOpen();
const testConnectionAction = this.page
.getByRole("button", { name: "Launch scan", exact: true })
.or(
@@ -1374,21 +1289,6 @@ export class ProvidersPage extends BasePage {
exact: true,
}),
);
// Some update flows return directly to providers list after authenticating.
try {
await Promise.race([
testConnectionAction.waitFor({ state: "visible", timeout: 20000 }),
this.providersTable.waitFor({ state: "visible", timeout: 20000 }),
]);
} catch {
// Fall through to explicit assertions below.
}
if (await this.providersTable.isVisible().catch(() => false)) {
return;
}
await expect(testConnectionAction).toBeVisible();
}
}
-57
View File
@@ -891,60 +891,3 @@
- Requires valid Alibaba Cloud account with RAM Role configured
- RAM Role must have sufficient permissions for security scanning
- Role ARN must be properly configured and assumable
---
## Test Case: `PROVIDER-E2E-016` - Add AWS Organization Using AWS Organizations Flow
**Priority:** `critical`
**Tags:**
- type → @e2e, @serial
- feature → @providers
- provider → @aws
**Description/Objective:** Validates the complete flow of adding AWS accounts through AWS Organizations, including organization setup, authentication, account selection, and scan scheduling.
**Preconditions:**
- Admin user authentication required (admin.auth.setup setup)
- Environment variables configured: E2E_AWS_ORGANIZATION_ID, E2E_AWS_ORGANIZATION_ROLE_ARN
- Remove any existing provider with the same Organization ID before starting the test
- StackSet must be deployed in AWS Organizations and expose a valid IAM Role ARN for Prowler
- This test must be run serially and never in parallel with other tests, as it requires the Organization ID not to be already registered beforehand.
### Flow Steps:
1. Navigate to providers page
2. Click "Add Provider" button
3. Select AWS provider type
4. Select "Add Multiple Accounts With AWS Organizations"
5. Fill organization details (organization ID and optional name)
6. Continue to authentication details and provide role ARN
7. Confirm StackSet deployment checkbox and authenticate
8. Confirm organization account selection step and continue
9. Verify organization launch step, choose single scan schedule, and launch
10. Verify redirect to Scans page
### Expected Result:
- AWS Organizations flow completes successfully
- Accounts are connected and launch step is displayed
- Scan scheduling selection is applied
- User is redirected to Scans page after launch
### Key verification points:
- Connect account page displays AWS option
- Organizations method selector is available
- Authentication details step loads
- Account selection step loads
- Accounts connected launch step appears
- Successful redirect to Scans page after launching
### Notes:
- Organization ID must follow AWS format (e.g., o-abc123def4)
- Role ARN must belong to the StackSet deployment for Organizations flow
- Provider cleanup is executed before test run to avoid unique constraint conflicts
+138 -150
View File
@@ -2,8 +2,6 @@ import { test } from "@playwright/test";
import {
ProvidersPage,
AWSProviderData,
AWSOrganizationsProviderCredential,
AWSOrganizationsProviderData,
AWSProviderCredential,
AWS_CREDENTIAL_OPTIONS,
AZUREProviderData,
@@ -38,18 +36,23 @@ test.describe("Add Provider", () => {
let providersPage: ProvidersPage;
let scansPage: ScansPage;
// Test data from environment variables
const accountId = process.env.E2E_AWS_PROVIDER_ACCOUNT_ID ?? "";
const accessKey = process.env.E2E_AWS_PROVIDER_ACCESS_KEY ?? "";
const secretKey = process.env.E2E_AWS_PROVIDER_SECRET_KEY ?? "";
const roleArn = process.env.E2E_AWS_PROVIDER_ROLE_ARN ?? "";
const organizationId = process.env.E2E_AWS_ORGANIZATION_ID ?? "";
const organizationRoleArn = process.env.E2E_AWS_ORGANIZATION_ROLE_ARN ?? "";
const accountId = process.env.E2E_AWS_PROVIDER_ACCOUNT_ID;
const accessKey = process.env.E2E_AWS_PROVIDER_ACCESS_KEY;
const secretKey = process.env.E2E_AWS_PROVIDER_SECRET_KEY;
const roleArn = process.env.E2E_AWS_PROVIDER_ROLE_ARN;
// Validate required environment variables
if (!accountId) {
throw new Error(
"E2E_AWS_PROVIDER_ACCOUNT_ID environment variable is not set",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(!accountId, "E2E_AWS_PROVIDER_ACCOUNT_ID is not set");
providersPage = new ProvidersPage(page);
await deleteProviderIfExists(providersPage, accountId!);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, accountId);
});
// Use admin authentication for provider management
@@ -213,8 +216,9 @@ test.describe("Add Provider", () => {
],
},
async ({ page }) => {
// Validate required environment variables
if (!accountId || !roleArn) {
if (!accountId || !roleArn) {
throw new Error(
"E2E_AWS_PROVIDER_ACCOUNT_ID, and E2E_AWS_PROVIDER_ROLE_ARN environment variables are not set",
);
@@ -274,73 +278,6 @@ test.describe("Add Provider", () => {
await scansPage.verifyScheduledScanStatus(accountId);
},
);
test(
"should add multiple AWS accounts using AWS Organizations",
{
tag: [
"@critical",
"@e2e",
"@providers",
"@aws",
"@serial",
"@PROVIDER-E2E-016",
],
},
async ({ page }) => {
if (!organizationId || !organizationRoleArn) {
test.skip(
true,
"E2E_AWS_ORGANIZATION_ID and E2E_AWS_ORGANIZATION_ROLE_ARN environment variables are not set",
);
return;
}
const awsOrganizationId = organizationId;
const awsOrganizationRoleArn = organizationRoleArn;
await deleteProviderIfExists(providersPage, awsOrganizationId);
const awsOrganizationData: AWSOrganizationsProviderData = {
organizationId: awsOrganizationId,
organizationName: "Test E2E AWS Organization",
};
const organizationsCredentials: AWSOrganizationsProviderCredential = {
roleArn: awsOrganizationRoleArn,
stackSetDeployed: true,
};
await providersPage.goto();
await providersPage.verifyPageLoaded();
await providersPage.clickAddProvider();
await providersPage.verifyConnectAccountPageLoaded();
await providersPage.selectAWSProvider();
await providersPage.selectAWSOrganizationsMethod();
await providersPage.fillAWSOrganizationsProviderDetails(
awsOrganizationData,
);
await providersPage.clickNext();
await providersPage.verifyOrganizationsAuthenticationStepLoaded();
await providersPage.fillAWSOrganizationsCredentials(
organizationsCredentials,
);
await providersPage.clickNext();
await providersPage.verifyOrganizationsAccountSelectionStepLoaded();
await providersPage.clickNext();
await providersPage.verifyOrganizationsLaunchStepLoaded();
await providersPage.chooseOrganizationsScanSchedule("single");
await providersPage.clickNext();
scansPage = new ScansPage(page);
await scansPage.verifyPageLoaded();
},
);
});
test.describe.serial("Add AZURE Provider", () => {
@@ -349,19 +286,23 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const subscriptionId = process.env.E2E_AZURE_SUBSCRIPTION_ID ?? "";
const clientId = process.env.E2E_AZURE_CLIENT_ID ?? "";
const clientSecret = process.env.E2E_AZURE_SECRET_ID ?? "";
const tenantId = process.env.E2E_AZURE_TENANT_ID ?? "";
const subscriptionId = process.env.E2E_AZURE_SUBSCRIPTION_ID;
const clientId = process.env.E2E_AZURE_CLIENT_ID;
const clientSecret = process.env.E2E_AZURE_SECRET_ID;
const tenantId = process.env.E2E_AZURE_TENANT_ID;
// Validate required environment variables
if (!subscriptionId || !clientId || !clientSecret || !tenantId) {
throw new Error(
"E2E_AZURE_SUBSCRIPTION_ID, E2E_AZURE_CLIENT_ID, E2E_AZURE_SECRET_ID, and E2E_AZURE_TENANT_ID environment variables are not set",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(
!subscriptionId || !clientId || !clientSecret || !tenantId,
"Azure E2E env vars are not set",
);
providersPage = new ProvidersPage(page);
await deleteProviderIfExists(providersPage, subscriptionId!);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, subscriptionId);
});
// Use admin authentication for provider management
@@ -433,18 +374,22 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const domainId = process.env.E2E_M365_DOMAIN_ID ?? "";
const clientId = process.env.E2E_M365_CLIENT_ID ?? "";
const tenantId = process.env.E2E_M365_TENANT_ID ?? "";
const domainId = process.env.E2E_M365_DOMAIN_ID;
const clientId = process.env.E2E_M365_CLIENT_ID;
const tenantId = process.env.E2E_M365_TENANT_ID;
// Validate required environment variables
if (!domainId || !clientId || !tenantId) {
throw new Error(
"E2E_M365_DOMAIN_ID, E2E_M365_CLIENT_ID, and E2E_M365_TENANT_ID environment variables are not set",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(
!domainId || !clientId || !tenantId,
"M365 E2E env vars are not set",
);
providersPage = new ProvidersPage(page);
await deleteProviderIfExists(providersPage, domainId!);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, domainId);
});
// Use admin authentication for provider management
@@ -464,7 +409,7 @@ test.describe("Add Provider", () => {
},
async ({ page }) => {
// Validate required environment variables
const clientSecret = process.env.E2E_M365_SECRET_ID ?? "";
const clientSecret = process.env.E2E_M365_SECRET_ID;
if (!clientSecret) {
throw new Error("E2E_M365_SECRET_ID environment variable is not set");
@@ -537,8 +482,7 @@ test.describe("Add Provider", () => {
},
async ({ page }) => {
// Validate required environment variables
const certificateContent =
process.env.E2E_M365_CERTIFICATE_CONTENT ?? "";
const certificateContent = process.env.E2E_M365_CERTIFICATE_CONTENT;
if (!certificateContent) {
throw new Error(
@@ -607,17 +551,22 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const context = process.env.E2E_KUBERNETES_CONTEXT ?? "";
const kubeconfigPath = process.env.E2E_KUBERNETES_KUBECONFIG_PATH ?? "";
const context = process.env.E2E_KUBERNETES_CONTEXT;
const kubeconfigPath = process.env.E2E_KUBERNETES_KUBECONFIG_PATH;
// Validate required environment variables
if (!context || !kubeconfigPath) {
throw new Error(
"E2E_KUBERNETES_CONTEXT and E2E_KUBERNETES_KUBECONFIG_PATH environment variables are not set",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(
!context || !kubeconfigPath,
"Kubernetes E2E env vars are not set",
);
providersPage = new ProvidersPage(page);
await deleteProviderIfExists(providersPage, context!);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, context);
});
// Use admin authentication for provider management
@@ -707,13 +656,18 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const projectId = process.env.E2E_GCP_PROJECT_ID ?? "";
const projectId = process.env.E2E_GCP_PROJECT_ID;
// Validate required environment variables
if (!projectId) {
throw new Error("E2E_GCP_PROJECT_ID environment variable is not set");
}
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(!projectId, "E2E_GCP_PROJECT_ID is not set");
providersPage = new ProvidersPage(page);
await deleteProviderIfExists(providersPage, projectId!);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, projectId);
});
// Use admin authentication for provider management
@@ -734,7 +688,7 @@ test.describe("Add Provider", () => {
async ({ page }) => {
// Validate required environment variables
const serviceAccountKeyB64 =
process.env.E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY ?? "";
process.env.E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY;
// Verify service account key is base64 encoded
if (!serviceAccountKeyB64) {
@@ -813,13 +767,19 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
test.describe("Add GitHub provider with username", () => {
const username = process.env.E2E_GITHUB_USERNAME ?? "";
// Test data from environment variables
const username = process.env.E2E_GITHUB_USERNAME;
// Validate required environment variables
if (!username) {
throw new Error("E2E_GITHUB_USERNAME environment variable is not set");
}
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(!username, "E2E_GITHUB_USERNAME is not set");
providersPage = new ProvidersPage(page);
await deleteProviderIfExists(providersPage, username!);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, username);
});
// Use admin authentication for provider management
@@ -840,7 +800,7 @@ test.describe("Add Provider", () => {
async ({ page }) => {
// Validate required environment variables
const personalAccessToken =
process.env.E2E_GITHUB_PERSONAL_ACCESS_TOKEN ?? "";
process.env.E2E_GITHUB_PERSONAL_ACCESS_TOKEN;
// Verify username and personal access token are set in environment variables
if (!personalAccessToken) {
@@ -916,9 +876,10 @@ test.describe("Add Provider", () => {
},
async ({ page }) => {
// Validate required environment variables
const githubAppId = process.env.E2E_GITHUB_APP_ID ?? "";
const githubAppId =
process.env.E2E_GITHUB_APP_ID;
const githubAppPrivateKeyB64 =
process.env.E2E_GITHUB_BASE64_APP_PRIVATE_KEY ?? "";
process.env.E2E_GITHUB_BASE64_APP_PRIVATE_KEY;
// Verify github app id and private key are set in environment variables
if (!githubAppId || !githubAppPrivateKeyB64) {
@@ -969,7 +930,9 @@ test.describe("Add Provider", () => {
await providersPage.verifyGitHubAppPageLoaded();
// Fill static github app credentials details
await providersPage.fillGitHubAppCredentials(githubCredentials);
await providersPage.fillGitHubAppCredentials(
githubCredentials,
);
await providersPage.clickNext();
// Launch scan
@@ -986,13 +949,21 @@ test.describe("Add Provider", () => {
);
});
test.describe("Add GitHub provider with organization", () => {
const organization = process.env.E2E_GITHUB_ORGANIZATION ?? "";
// Test data from environment variables
const organization = process.env.E2E_GITHUB_ORGANIZATION;
// Validate required environment variables
if (!organization) {
throw new Error(
"E2E_GITHUB_ORGANIZATION environment variable is not set",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(!organization, "E2E_GITHUB_ORGANIZATION is not set");
providersPage = new ProvidersPage(page);
await deleteProviderIfExists(providersPage, organization!);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, organization);
});
// Use admin authentication for provider management
@@ -1012,7 +983,7 @@ test.describe("Add Provider", () => {
async ({ page }) => {
// Validate required environment variables
const organizationAccessToken =
process.env.E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN ?? "";
process.env.E2E_GITHUB_ORGANIZATION_ACCESS_TOKEN;
// Verify username and personal access token are set in environment variables
if (!organizationAccessToken) {
@@ -1083,20 +1054,24 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const tenancyId = process.env.E2E_OCI_TENANCY_ID ?? "";
const userId = process.env.E2E_OCI_USER_ID ?? "";
const fingerprint = process.env.E2E_OCI_FINGERPRINT ?? "";
const keyContent = process.env.E2E_OCI_KEY_CONTENT ?? "";
const region = process.env.E2E_OCI_REGION ?? "";
const tenancyId = process.env.E2E_OCI_TENANCY_ID;
const userId = process.env.E2E_OCI_USER_ID;
const fingerprint = process.env.E2E_OCI_FINGERPRINT;
const keyContent = process.env.E2E_OCI_KEY_CONTENT;
const region = process.env.E2E_OCI_REGION;
// Validate required environment variables
if (!tenancyId || !userId || !fingerprint || !keyContent || !region) {
throw new Error(
"E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, and E2E_OCI_REGION environment variables are not set",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(
!tenancyId || !userId || !fingerprint || !keyContent || !region,
"OCI E2E env vars are not set",
);
providersPage = new ProvidersPage(page);
await deleteProviderIfExists(providersPage, tenancyId!);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, tenancyId);
});
// Use admin authentication for provider management
@@ -1173,17 +1148,23 @@ test.describe("Add Provider", () => {
let scansPage: ScansPage;
// Test data from environment variables
const accountId = process.env.E2E_ALIBABACLOUD_ACCOUNT_ID ?? "";
const accessKeyId = process.env.E2E_ALIBABACLOUD_ACCESS_KEY_ID ?? "";
const accessKeySecret =
process.env.E2E_ALIBABACLOUD_ACCESS_KEY_SECRET ?? "";
const roleArn = process.env.E2E_ALIBABACLOUD_ROLE_ARN ?? "";
const accountId = process.env.E2E_ALIBABACLOUD_ACCOUNT_ID;
const accessKeyId = process.env.E2E_ALIBABACLOUD_ACCESS_KEY_ID;
const accessKeySecret = process.env.E2E_ALIBABACLOUD_ACCESS_KEY_SECRET;
const roleArn = process.env.E2E_ALIBABACLOUD_ROLE_ARN;
// Validate required environment variable for beforeEach
if (!accountId) {
throw new Error(
"E2E_ALIBABACLOUD_ACCOUNT_ID environment variable is not set",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(!accountId, "E2E_ALIBABACLOUD_ACCOUNT_ID is not set");
providersPage = new ProvidersPage(page);
await deleteProviderIfExists(providersPage, accountId!);
// Clean up existing provider to ensure clean test state
await deleteProviderIfExists(providersPage, accountId);
});
// Use admin authentication for provider management
@@ -1251,9 +1232,7 @@ test.describe("Add Provider", () => {
await providersPage.verifyAlibabaCloudStaticCredentialsPageLoaded();
// Fill static credentials
await providersPage.fillAlibabaCloudStaticCredentials(
staticCredentials,
);
await providersPage.fillAlibabaCloudStaticCredentials(staticCredentials);
await providersPage.clickNext();
// Launch scan
@@ -1355,18 +1334,21 @@ test.describe("Update Provider Credentials", () => {
let providersPage: ProvidersPage;
// Test data from environment variables (same as add OCI provider test)
const tenancyId = process.env.E2E_OCI_TENANCY_ID ?? "";
const userId = process.env.E2E_OCI_USER_ID ?? "";
const fingerprint = process.env.E2E_OCI_FINGERPRINT ?? "";
const keyContent = process.env.E2E_OCI_KEY_CONTENT ?? "";
const region = process.env.E2E_OCI_REGION ?? "";
const tenancyId = process.env.E2E_OCI_TENANCY_ID;
const userId = process.env.E2E_OCI_USER_ID;
const fingerprint = process.env.E2E_OCI_FINGERPRINT;
const keyContent = process.env.E2E_OCI_KEY_CONTENT;
const region = process.env.E2E_OCI_REGION;
// Validate required environment variables
if (!tenancyId || !userId || !fingerprint || !keyContent || !region) {
throw new Error(
"E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, and E2E_OCI_REGION environment variables are not set",
);
}
// Setup before each test
test.beforeEach(async ({ page }) => {
test.skip(
!tenancyId || !userId || !fingerprint || !keyContent || !region,
"OCI E2E env vars are not set",
);
providersPage = new ProvidersPage(page);
});
@@ -1376,7 +1358,13 @@ test.describe("Update Provider Credentials", () => {
test(
"should update OCI provider credentials successfully",
{
tag: ["@e2e", "@providers", "@oci", "@serial", "@PROVIDER-E2E-013"],
tag: [
"@e2e",
"@providers",
"@oci",
"@serial",
"@PROVIDER-E2E-013",
],
},
async () => {
// Prepare updated credentials
+3 -1
View File
@@ -133,7 +133,9 @@ export class SignUpPage extends BasePage {
}
async verifyRedirectToLogin(): Promise<void> {
await expect(this.page).toHaveURL(/\/sign-in/);
// Verify redirect to login page
await expect(this.page).toHaveURL("/sign-in");
}
async verifyRedirectToEmailVerification(): Promise<void> {
-48
View File
@@ -1,48 +0,0 @@
import { describe, expect, it } from "vitest";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { addCredentialsRoleFormSchema } from "./formSchemas";
const BASE_AWS_ROLE_VALUES = {
[ProviderCredentialFields.PROVIDER_ID]: "provider-123",
[ProviderCredentialFields.PROVIDER_TYPE]: "aws",
[ProviderCredentialFields.ROLE_ARN]:
"arn:aws:iam::123456789012:role/ProwlerRole",
[ProviderCredentialFields.EXTERNAL_ID]: "tenant-123",
[ProviderCredentialFields.CREDENTIALS_TYPE]: "access-secret-key",
} as const;
describe("addCredentialsRoleFormSchema", () => {
it("accepts AWS role credentials when access and secret keys are present", () => {
const schema = addCredentialsRoleFormSchema("aws");
const result = schema.safeParse({
...BASE_AWS_ROLE_VALUES,
[ProviderCredentialFields.AWS_ACCESS_KEY_ID]: "AKIA1234567890EXAMPLE",
[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]:
"test/secret+access=key1234567890",
});
expect(result.success).toBe(true);
});
it("reports missing AWS secret access key on aws_secret_access_key field", () => {
const schema = addCredentialsRoleFormSchema("aws");
const result = schema.safeParse({
...BASE_AWS_ROLE_VALUES,
[ProviderCredentialFields.AWS_ACCESS_KEY_ID]: "AKIA1234567890EXAMPLE",
[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]: "",
});
expect(result.success).toBe(false);
if (result.success) return;
expect(result.error.issues).toContainEqual(
expect.objectContaining({
path: [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY],
}),
);
});
});
+10 -30
View File
@@ -420,37 +420,17 @@ export const addCredentialsRoleFormSchema = (providerType: string) =>
[ProviderCredentialFields.ROLE_SESSION_NAME]: z.string().optional(),
[ProviderCredentialFields.CREDENTIALS_TYPE]: z.string().optional(),
})
.superRefine((data, ctx) => {
if (
.refine(
(data) =>
data[ProviderCredentialFields.CREDENTIALS_TYPE] !==
"access-secret-key"
) {
return;
}
const hasAccessKey =
(data[ProviderCredentialFields.AWS_ACCESS_KEY_ID] || "").trim()
.length > 0;
const hasSecretAccessKey =
(data[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY] || "").trim()
.length > 0;
if (!hasAccessKey) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "AWS Access Key ID is required.",
path: [ProviderCredentialFields.AWS_ACCESS_KEY_ID],
});
}
if (!hasSecretAccessKey) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "AWS Secret Access Key is required.",
path: [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY],
});
}
})
"access-secret-key" ||
(data[ProviderCredentialFields.AWS_ACCESS_KEY_ID] &&
data[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]),
{
message: "AWS Access Key ID and Secret Access Key are required.",
path: [ProviderCredentialFields.AWS_ACCESS_KEY_ID],
},
)
: providerType === "alibabacloud"
? z.object({
[ProviderCredentialFields.PROVIDER_ID]: z.string(),