mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
feat(ui): improve organizations onboarding (#10274)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
? `¶m_EnableS3Integration=true¶m_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¶m_ExternalId=${externalId}${bucketName ? `¶m_EnableS3Integration=true¶m_S3IntegrationBucketName=${bucketName}` : ""}`,
|
||||
cloudformationQuickLink:
|
||||
`${cfBaseUrl}?templateURL=${encodedTemplateUrl}` +
|
||||
`&stackName=Prowler¶m_ExternalId=${externalId}${s3Params}`,
|
||||
cloudformationOrgQuickLink:
|
||||
`${cfBaseUrl}?templateURL=${encodedTemplateUrl}` +
|
||||
`&stackName=Prowler¶m_ExternalId=${externalId}` +
|
||||
`¶m_EnableOrganizations=true${s3Params}`,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user