feat(ui): improve organizations onboarding (#10274)

This commit is contained in:
Pedro Martín
2026-03-09 16:54:50 +01:00
committed by GitHub
parent 809142de35
commit 23a8d4e680
3 changed files with 132 additions and 50 deletions

View File

@@ -4,10 +4,13 @@ All notable changes to the **Prowler UI** are documented in this file.
## [1.20.0] (Prowler v5.20.0 UNRELEASED)
### 🐞 Changed
### 🔄 Changed
- Attack Paths: Improved error handling for server errors (5xx) and network failures with user-friendly messages instead of raw internal errors and layout changes. [(#10249)](https://github.com/prowler-cloud/prowler/pull/10249)
- Refactor simple providers with new components and styles.[(#10259)](https://github.com/prowler-cloud/prowler/pull/10259)
- AWS Organizations onboarding now uses a clearer 3-step flow: deploy the ProwlerScan role in the management account via CloudFormation Stack, deploy to member accounts via StackSet with a copyable template URL, and confirm with the Role ARN [(#10274)](https://github.com/prowler-cloud/prowler/pull/10274)
---
## [1.19.1] (Prowler v5.19.1 UNRELEASED)

View File

@@ -1,5 +1,6 @@
"use client";
import { useClipboard } from "@heroui/use-clipboard";
import { zodResolver } from "@hookform/resolvers/zod";
import { Check, Copy, ExternalLink } from "lucide-react";
import { useSession } from "next-auth/react";
@@ -18,7 +19,11 @@ import { Button } from "@/components/shadcn/button/button";
import { Checkbox } from "@/components/shadcn/checkbox/checkbox";
import { TreeSpinner } from "@/components/shadcn/tree-view/tree-spinner";
import { Form } from "@/components/ui/form";
import { getAWSCredentialsTemplateLinks } from "@/lib";
import {
getAWSCredentialsTemplateLinks,
PROWLER_CF_TEMPLATE_URL,
STACKSET_CONSOLE_URL,
} from "@/lib";
import { ORG_SETUP_PHASE, OrgSetupPhase } from "@/types/organizations";
import { useOrgSetupSubmission } from "./hooks/use-org-setup-submission";
@@ -39,7 +44,7 @@ const orgSetupSchema = z.object({
.min(1, "Role ARN is required")
.regex(
/^arn:aws:iam::\d{12}:role\//,
"Must be a valid IAM Role ARN (e.g., arn:aws:iam::123456789012:role/ProwlerOrgRole)",
"Must be a valid IAM Role ARN (e.g., arn:aws:iam::123456789012:role/ProwlerScan)",
),
stackSetDeployed: z.boolean().refine((value) => value, {
message: "You must confirm the StackSet deployment before continuing.",
@@ -64,8 +69,13 @@ export function OrgSetupForm({
initialPhase = ORG_SETUP_PHASE.DETAILS,
}: OrgSetupFormProps) {
const { data: session } = useSession();
const [isExternalIdCopied, setIsExternalIdCopied] = useState(false);
const stackSetExternalId = session?.tenantId ?? "";
const { copied: isExternalIdCopied, copy: copyExternalId } = useClipboard({
timeout: 1500,
});
const { copied: isTemplateUrlCopied, copy: copyTemplateUrl } = useClipboard({
timeout: 1500,
});
const [setupPhase, setSetupPhase] = useState<OrgSetupPhase>(initialPhase);
const formId = "org-wizard-setup-form";
@@ -90,9 +100,10 @@ export function OrgSetupForm({
const awsOrgId = watch("awsOrgId") || "";
const isOrgIdValid = /^o-[a-z0-9]{10,32}$/.test(awsOrgId.trim());
const stackSetQuickLink =
stackSetExternalId &&
getAWSCredentialsTemplateLinks(stackSetExternalId).cloudformationQuickLink;
const templateLinks = stackSetExternalId
? getAWSCredentialsTemplateLinks(stackSetExternalId)
: null;
const orgQuickLink = templateLinks?.cloudformationOrgQuickLink;
const { apiError, setApiError, submitOrganizationSetup } =
useOrgSetupSubmission({
@@ -260,35 +271,11 @@ export function OrgSetupForm({
{setupPhase === ORG_SETUP_PHASE.ACCESS && !isSubmitting && (
<div className="flex flex-col gap-8">
{/* External ID - shown first for both deployment steps */}
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
1) Launch the Prowler CloudFormation StackSet in your AWS
Console.
</p>
<Button
variant="outline"
size="lg"
className="border-border-input-primary bg-bg-input-primary text-button-tertiary hover:bg-bg-input-primary active:bg-bg-input-primary h-12 w-full justify-start"
disabled={!stackSetQuickLink}
asChild
>
<a
href={stackSetQuickLink || "#"}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="size-5" />
<span>
Prowler CloudFormation StackSet for AWS Organizations
</span>
</a>
</Button>
</div>
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
2) Use the following Prowler External ID parameter in the
StackSet.
Use the following <strong>External ID</strong> when deploying
the CloudFormation Stack and StackSet.
</p>
<div className="flex items-center gap-3">
<span className="text-text-neutral-tertiary text-xs">
@@ -302,15 +289,7 @@ export function OrgSetupForm({
<button
type="button"
disabled={!stackSetExternalId}
onClick={async () => {
try {
await navigator.clipboard.writeText(stackSetExternalId);
setIsExternalIdCopied(true);
setTimeout(() => setIsExternalIdCopied(false), 1500);
} catch {
// Ignore clipboard errors (e.g., unsupported browser context).
}
}}
onClick={() => copyExternalId(stackSetExternalId)}
className="text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 transition-colors"
aria-label="Copy external ID"
>
@@ -324,20 +303,93 @@ export function OrgSetupForm({
</div>
</div>
{/* Step 1: Management account - CloudFormation Stack */}
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
3) Copy the Prowler IAM Role ARN from AWS and confirm the
StackSet is successfully deployed by clicking the checkbox
below.
1) Deploy the ProwlerScan role in your{" "}
<strong>management account</strong> using a CloudFormation
Stack.
</p>
<Button
variant="outline"
size="lg"
className="border-border-input-primary bg-bg-input-primary text-button-tertiary hover:bg-bg-input-primary active:bg-bg-input-primary h-12 w-full justify-start"
disabled={!orgQuickLink}
asChild
>
<a
href={orgQuickLink || "#"}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="size-5" />
<span>Create Stack in Management Account</span>
</a>
</Button>
</div>
{/* Step 2: Member accounts - CloudFormation StackSet */}
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
2) Deploy the ProwlerScan role to{" "}
<strong>member accounts</strong> using a CloudFormation
StackSet.
</p>
<p className="text-text-neutral-tertiary text-xs leading-5">
Open the StackSets console, select{" "}
<strong>Service-managed permissions</strong>, and paste the
template URL below. Set the <strong>ExternalId</strong>{" "}
parameter to the value shown above.
</p>
<div className="bg-bg-neutral-tertiary border-border-input-primary flex items-center gap-3 rounded-lg border px-4 py-2.5">
<span className="text-text-neutral-primary min-w-0 flex-1 truncate font-mono text-xs">
{PROWLER_CF_TEMPLATE_URL}
</span>
<button
type="button"
onClick={() => copyTemplateUrl(PROWLER_CF_TEMPLATE_URL)}
className="text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 transition-colors"
aria-label="Copy template URL"
>
{isTemplateUrlCopied ? (
<Check className="size-4" />
) : (
<Copy className="size-4" />
)}
</button>
</div>
<Button
variant="outline"
size="lg"
className="border-border-input-primary bg-bg-input-primary text-button-tertiary hover:bg-bg-input-primary active:bg-bg-input-primary h-12 w-full justify-start"
disabled={!isExternalIdCopied}
asChild
>
<a
href={STACKSET_CONSOLE_URL}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="size-5" />
<span>Open StackSets Console</span>
</a>
</Button>
</div>
{/* Step 3: Role ARN + confirm */}
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
3) Paste the management account Role ARN and confirm both
deployments are complete.
</p>
</div>
<WizardInputField
control={control}
name="roleArn"
label="Role ARN"
label="Management Account Role ARN"
labelPlacement="outside"
placeholder="e.g. arn:aws:iam::123456789012:role/ProwlerOrgRole"
placeholder="e.g. arn:aws:iam::123456789012:role/ProwlerScan"
isRequired={false}
requiredIndicator
/>
@@ -365,7 +417,8 @@ export function OrgSetupForm({
htmlFor="stackSetDeployed"
className="text-text-neutral-tertiary text-xs leading-5 font-normal"
>
The StackSet has been successfully deployed in AWS
The Stack and StackSet have been successfully deployed in
AWS
<span className="text-text-error-primary">*</span>
</label>
</>

View File

@@ -8,6 +8,18 @@ export const DOCS_URLS = {
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
} as const;
// CloudFormation template URL for the ProwlerScan role.
// Also used (URL-encoded) as the templateURL param in cloudformationQuickLink
// and cloudformationOrgQuickLink below — keep both in sync.
export const PROWLER_CF_TEMPLATE_URL =
"https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml";
// AWS Console URL for creating a new StackSet.
// Hardcoded to us-east-1 — StackSets are typically managed from this region.
// Users in AWS GovCloud or China partitions would need different URLs.
export const STACKSET_CONSOLE_URL =
"https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacksets/create";
export const getProviderHelpText = (provider: string) => {
switch (provider) {
case "aws":
@@ -86,6 +98,7 @@ export const getAWSCredentialsTemplateLinks = (
cloudformation: string;
terraform: string;
cloudformationQuickLink: string;
cloudformationOrgQuickLink: string;
} => {
let links = {};
@@ -107,11 +120,24 @@ export const getAWSCredentialsTemplateLinks = (
};
}
const encodedTemplateUrl = encodeURIComponent(PROWLER_CF_TEMPLATE_URL);
const cfBaseUrl =
"https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate";
const s3Params = bucketName
? `&param_EnableS3Integration=true&param_S3IntegrationBucketName=${bucketName}`
: "";
return {
...(links as {
cloudformation: string;
terraform: string;
}),
cloudformationQuickLink: `https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https%3A%2F%2Fprowler-cloud-public.s3.eu-west-1.amazonaws.com%2Fpermissions%2Ftemplates%2Faws%2Fcloudformation%2Fprowler-scan-role.yml&stackName=Prowler&param_ExternalId=${externalId}${bucketName ? `&param_EnableS3Integration=true&param_S3IntegrationBucketName=${bucketName}` : ""}`,
cloudformationQuickLink:
`${cfBaseUrl}?templateURL=${encodedTemplateUrl}` +
`&stackName=Prowler&param_ExternalId=${externalId}${s3Params}`,
cloudformationOrgQuickLink:
`${cfBaseUrl}?templateURL=${encodedTemplateUrl}` +
`&stackName=Prowler&param_ExternalId=${externalId}` +
`&param_EnableOrganizations=true${s3Params}`,
};
};