11"use client" ;
22
3+ import { useClipboard } from "@heroui/use-clipboard" ;
34import { zodResolver } from "@hookform/resolvers/zod" ;
45import { Check , Copy , ExternalLink } from "lucide-react" ;
56import { useSession } from "next-auth/react" ;
@@ -18,7 +19,11 @@ import { Button } from "@/components/shadcn/button/button";
1819import { Checkbox } from "@/components/shadcn/checkbox/checkbox" ;
1920import { TreeSpinner } from "@/components/shadcn/tree-view/tree-spinner" ;
2021import { Form } from "@/components/ui/form" ;
21- import { getAWSCredentialsTemplateLinks } from "@/lib" ;
22+ import {
23+ getAWSCredentialsTemplateLinks ,
24+ PROWLER_CF_TEMPLATE_URL ,
25+ STACKSET_CONSOLE_URL ,
26+ } from "@/lib" ;
2227import { ORG_SETUP_PHASE , OrgSetupPhase } from "@/types/organizations" ;
2328
2429import { useOrgSetupSubmission } from "./hooks/use-org-setup-submission" ;
@@ -39,7 +44,7 @@ const orgSetupSchema = z.object({
3944 . min ( 1 , "Role ARN is required" )
4045 . regex (
4146 / ^ a r n : a w s : i a m : : \d { 12 } : r o l e \/ / ,
42- "Must be a valid IAM Role ARN (e.g., arn:aws:iam::123456789012:role/ProwlerOrgRole )" ,
47+ "Must be a valid IAM Role ARN (e.g., arn:aws:iam::123456789012:role/ProwlerScan )" ,
4348 ) ,
4449 stackSetDeployed : z . boolean ( ) . refine ( ( value ) => value , {
4550 message : "You must confirm the StackSet deployment before continuing." ,
@@ -64,8 +69,13 @@ export function OrgSetupForm({
6469 initialPhase = ORG_SETUP_PHASE . DETAILS ,
6570} : OrgSetupFormProps ) {
6671 const { data : session } = useSession ( ) ;
67- const [ isExternalIdCopied , setIsExternalIdCopied ] = useState ( false ) ;
6872 const stackSetExternalId = session ?. tenantId ?? "" ;
73+ const { copied : isExternalIdCopied , copy : copyExternalId } = useClipboard ( {
74+ timeout : 1500 ,
75+ } ) ;
76+ const { copied : isTemplateUrlCopied , copy : copyTemplateUrl } = useClipboard ( {
77+ timeout : 1500 ,
78+ } ) ;
6979 const [ setupPhase , setSetupPhase ] = useState < OrgSetupPhase > ( initialPhase ) ;
7080 const formId = "org-wizard-setup-form" ;
7181
@@ -90,9 +100,10 @@ export function OrgSetupForm({
90100
91101 const awsOrgId = watch ( "awsOrgId" ) || "" ;
92102 const isOrgIdValid = / ^ o - [ a - z 0 - 9 ] { 10 , 32 } $ / . test ( awsOrgId . trim ( ) ) ;
93- const stackSetQuickLink =
94- stackSetExternalId &&
95- getAWSCredentialsTemplateLinks ( stackSetExternalId ) . cloudformationQuickLink ;
103+ const templateLinks = stackSetExternalId
104+ ? getAWSCredentialsTemplateLinks ( stackSetExternalId )
105+ : null ;
106+ const orgQuickLink = templateLinks ?. cloudformationOrgQuickLink ;
96107
97108 const { apiError, setApiError, submitOrganizationSetup } =
98109 useOrgSetupSubmission ( {
@@ -260,35 +271,11 @@ export function OrgSetupForm({
260271
261272 { setupPhase === ORG_SETUP_PHASE . ACCESS && ! isSubmitting && (
262273 < div className = "flex flex-col gap-8" >
274+ { /* External ID - shown first for both deployment steps */ }
263275 < div className = "flex flex-col gap-4" >
264276 < p className = "text-text-neutral-primary text-sm leading-7 font-normal" >
265- 1) Launch the Prowler CloudFormation StackSet in your AWS
266- Console.
267- </ p >
268- < Button
269- variant = "outline"
270- size = "lg"
271- 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"
272- disabled = { ! stackSetQuickLink }
273- asChild
274- >
275- < a
276- href = { stackSetQuickLink || "#" }
277- target = "_blank"
278- rel = "noopener noreferrer"
279- >
280- < ExternalLink className = "size-5" />
281- < span >
282- Prowler CloudFormation StackSet for AWS Organizations
283- </ span >
284- </ a >
285- </ Button >
286- </ div >
287-
288- < div className = "flex flex-col gap-4" >
289- < p className = "text-text-neutral-primary text-sm leading-7 font-normal" >
290- 2) Use the following Prowler External ID parameter in the
291- StackSet.
277+ Use the following < strong > External ID</ strong > when deploying
278+ the CloudFormation Stack and StackSet.
292279 </ p >
293280 < div className = "flex items-center gap-3" >
294281 < span className = "text-text-neutral-tertiary text-xs" >
@@ -302,15 +289,7 @@ export function OrgSetupForm({
302289 < button
303290 type = "button"
304291 disabled = { ! stackSetExternalId }
305- onClick = { async ( ) => {
306- try {
307- await navigator . clipboard . writeText ( stackSetExternalId ) ;
308- setIsExternalIdCopied ( true ) ;
309- setTimeout ( ( ) => setIsExternalIdCopied ( false ) , 1500 ) ;
310- } catch {
311- // Ignore clipboard errors (e.g., unsupported browser context).
312- }
313- } }
292+ onClick = { ( ) => copyExternalId ( stackSetExternalId ) }
314293 className = "text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 transition-colors"
315294 aria-label = "Copy external ID"
316295 >
@@ -324,20 +303,93 @@ export function OrgSetupForm({
324303 </ div >
325304 </ div >
326305
306+ { /* Step 1: Management account - CloudFormation Stack */ }
307+ < div className = "flex flex-col gap-4" >
308+ < p className = "text-text-neutral-primary text-sm leading-7 font-normal" >
309+ 1) Deploy the ProwlerScan role in your{ " " }
310+ < strong > management account</ strong > using a CloudFormation
311+ Stack.
312+ </ p >
313+ < Button
314+ variant = "outline"
315+ size = "lg"
316+ 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"
317+ disabled = { ! orgQuickLink }
318+ asChild
319+ >
320+ < a
321+ href = { orgQuickLink || "#" }
322+ target = "_blank"
323+ rel = "noopener noreferrer"
324+ >
325+ < ExternalLink className = "size-5" />
326+ < span > Create Stack in Management Account</ span >
327+ </ a >
328+ </ Button >
329+ </ div >
330+
331+ { /* Step 2: Member accounts - CloudFormation StackSet */ }
332+ < div className = "flex flex-col gap-4" >
333+ < p className = "text-text-neutral-primary text-sm leading-7 font-normal" >
334+ 2) Deploy the ProwlerScan role to{ " " }
335+ < strong > member accounts</ strong > using a CloudFormation
336+ StackSet.
337+ </ p >
338+ < p className = "text-text-neutral-tertiary text-xs leading-5" >
339+ Open the StackSets console, select{ " " }
340+ < strong > Service-managed permissions</ strong > , and paste the
341+ template URL below. Set the < strong > ExternalId</ strong > { " " }
342+ parameter to the value shown above.
343+ </ p >
344+ < div className = "bg-bg-neutral-tertiary border-border-input-primary flex items-center gap-3 rounded-lg border px-4 py-2.5" >
345+ < span className = "text-text-neutral-primary min-w-0 flex-1 truncate font-mono text-xs" >
346+ { PROWLER_CF_TEMPLATE_URL }
347+ </ span >
348+ < button
349+ type = "button"
350+ onClick = { ( ) => copyTemplateUrl ( PROWLER_CF_TEMPLATE_URL ) }
351+ className = "text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 transition-colors"
352+ aria-label = "Copy template URL"
353+ >
354+ { isTemplateUrlCopied ? (
355+ < Check className = "size-4" />
356+ ) : (
357+ < Copy className = "size-4" />
358+ ) }
359+ </ button >
360+ </ div >
361+ < Button
362+ variant = "outline"
363+ size = "lg"
364+ 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"
365+ disabled = { ! isExternalIdCopied }
366+ asChild
367+ >
368+ < a
369+ href = { STACKSET_CONSOLE_URL }
370+ target = "_blank"
371+ rel = "noopener noreferrer"
372+ >
373+ < ExternalLink className = "size-5" />
374+ < span > Open StackSets Console</ span >
375+ </ a >
376+ </ Button >
377+ </ div >
378+
379+ { /* Step 3: Role ARN + confirm */ }
327380 < div className = "flex flex-col gap-4" >
328381 < p className = "text-text-neutral-primary text-sm leading-7 font-normal" >
329- 3) Copy the Prowler IAM Role ARN from AWS and confirm the
330- StackSet is successfully deployed by clicking the checkbox
331- below.
382+ 3) Paste the management account Role ARN and confirm both
383+ deployments are complete.
332384 </ p >
333385 </ div >
334386
335387 < WizardInputField
336388 control = { control }
337389 name = "roleArn"
338- label = "Role ARN"
390+ label = "Management Account Role ARN"
339391 labelPlacement = "outside"
340- placeholder = "e.g. arn:aws:iam::123456789012:role/ProwlerOrgRole "
392+ placeholder = "e.g. arn:aws:iam::123456789012:role/ProwlerScan "
341393 isRequired = { false }
342394 requiredIndicator
343395 />
@@ -365,7 +417,8 @@ export function OrgSetupForm({
365417 htmlFor = "stackSetDeployed"
366418 className = "text-text-neutral-tertiary text-xs leading-5 font-normal"
367419 >
368- The StackSet has been successfully deployed in AWS
420+ The Stack and StackSet have been successfully deployed in
421+ AWS
369422 < span className = "text-text-error-primary" > *</ span >
370423 </ label >
371424 </ >
0 commit comments