fix(ui): deep-link wizard docs link to provider authentication guide (#12218)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
lydiavilchez
2026-08-04 11:57:56 +02:00
committed by GitHub
co-authored by Daniel Barranquero alejandrobailo
parent 64c0cf900f
commit fb9d989be8
8 changed files with 726 additions and 98 deletions
@@ -0,0 +1 @@
`Add Provider` wizard documentation link targeting each provider's credentials section and selected authentication method
@@ -170,8 +170,10 @@ describe("useProviderWizardController", () => {
});
expect(result.current.modalTitle).toBe("Update Provider Credentials");
expect(result.current.isProviderFlow).toBe(true);
// Update mode enters at the credentials step, so the docs link scrolls
// the getting-started page to the credentials/authentication section.
expect(result.current.docsLink).toBe(
"https://goto.prowler.com/provider-aws",
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#step-3-set-up-aws-authentication",
);
const state = useProviderWizardStore.getState();
@@ -183,6 +185,36 @@ describe("useProviderWizardController", () => {
expect(state.mode).toBe(PROVIDER_WIZARD_MODE.UPDATE);
});
it("updates the credentials docs link when AWS assume role is selected", async () => {
const onOpenChange = vi.fn();
const { result } = renderHook(() =>
useProviderWizardController({
open: true,
onOpenChange,
initialData: {
providerId: "provider-1",
providerType: "aws",
providerUid: "111111111111",
providerAlias: "production",
secretId: null,
mode: PROVIDER_WIZARD_MODE.ADD,
},
}),
);
await waitFor(() => {
expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.CREDENTIALS);
});
act(() => {
useProviderWizardStore.getState().setVia("role");
});
expect(result.current.docsLink).toBe(
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#assume-role-recommended",
);
});
it("switches into and out of organizations flow", () => {
// Given
const onOpenChange = vi.fn();
@@ -106,6 +106,7 @@ export function useProviderWizardController({
setMode,
mode,
providerType,
via,
} = useProviderWizardStore();
const {
reset: resetOrgWizard,
@@ -269,7 +270,11 @@ export function useProviderWizardController({
const isProviderFlow = wizardVariant === WIZARD_VARIANT.PROVIDER;
const docsLink = isProviderFlow
? getProviderHelpText(providerTypeHint ?? providerType ?? "").link
? getProviderHelpText(
providerTypeHint ?? providerType ?? "",
currentStep,
via,
).link
: ORG_DOCS_URL[organizationType];
const resolvedFooterConfig: WizardFooterConfig = footerConfig;
const modalTitle = getProviderWizardModalTitle(mode);
@@ -119,7 +119,7 @@ export function ProviderWizardModal({
<Button variant="link" size="link-sm" className="h-auto p-0" asChild>
<a href={docsLink} target="_blank" rel="noopener noreferrer">
<ExternalLink className="size-3.5 shrink-0" />
<span>{`${docsDestination} documentation`}</span>
<span>{`${docsDestination} Documentation`}</span>
</a>
</Button>
</div>
@@ -1,7 +1,12 @@
import { describe, expect, it } from "vitest";
import { getProviderHelpText } from "@/lib/external-urls";
import { ORG_SETUP_PHASE, ORG_WIZARD_STEP } from "@/types/organizations";
import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard";
import {
PROVIDER_WIZARD_MODE,
PROVIDER_WIZARD_STEP,
} from "@/types/provider-wizard";
import { type KnownProviderType, PROVIDER_TYPES } from "@/types/providers";
import {
getOrganizationsStepperOffset,
@@ -61,6 +66,111 @@ describe("getProviderWizardDocsDestination", () => {
expect(destination).toBe("AWS");
});
it("returns a compact provider label for deep-linked getting-started URLs", () => {
const destination = getProviderWizardDocsDestination(
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws",
);
expect(destination).toBe("AWS");
});
it("returns a specific label for AWS assume role docs links", () => {
const destination = getProviderWizardDocsDestination(
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#assume-role-recommended",
);
expect(destination).toBe("AWS Assume Role");
});
it("returns a method-specific label for every subsection deep-link", () => {
// Locks the docsSectionLabelMap keys to the URLs the frontend emits.
// Adding a new (provider, method) subsection URL requires wiring a
// matching label here or the modal header regresses to just the provider
// name.
const cases: Array<[string, string]> = [
[
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#assume-role-recommended",
"AWS Assume Role",
],
[
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#credentials-static-access-keys",
"AWS Credentials",
],
[
"https://docs.prowler.com/user-guide/providers/microsoft365/getting-started-m365#application-certificate-authentication-recommended",
"M365 Certificate",
],
[
"https://docs.prowler.com/user-guide/providers/microsoft365/getting-started-m365#application-client-secret-authentication",
"M365 Client Secret",
],
[
"https://docs.prowler.com/user-guide/providers/alibabacloud/getting-started-alibabacloud#ram-role-assumption-recommended",
"Alibaba Cloud RAM Role",
],
[
"https://docs.prowler.com/user-guide/providers/alibabacloud/getting-started-alibabacloud#credentials-static-access-keys",
"Alibaba Cloud Credentials",
],
[
"https://docs.prowler.com/user-guide/providers/cloudflare/getting-started-cloudflare#user-api-token-authentication-recommended",
"Cloudflare API Token",
],
[
"https://docs.prowler.com/user-guide/providers/cloudflare/getting-started-cloudflare#api-key-and-email-authentication-legacy",
"Cloudflare API Key",
],
];
for (const [url, expected] of cases) {
expect(getProviderWizardDocsDestination(url)).toBe(expected);
}
});
it("ignores a #authentication anchor when deriving the label", () => {
// The credentials step falls back to a shortlink + anchor for providers
// without a dedicated auth page (Kubernetes). The label shown in the
// modal header must stay the provider name, not shift with the anchor.
const destination = getProviderWizardDocsDestination(
"https://goto.prowler.com/provider-k8s#authentication",
);
expect(destination).toBe("Kubernetes");
});
it("derives the provider label from the dedicated authentication docs URL", () => {
// On the credentials step providers with a standalone authentication.mdx
// (all except Kubernetes) point to `/providers/<slug>/authentication`.
// The label must come from the `<slug>` segment, not from the trailing
// "authentication" page name.
const destination = getProviderWizardDocsDestination(
"https://docs.prowler.com/user-guide/providers/aws/authentication",
);
expect(destination).toBe("AWS");
});
it("maps the OCI docs slug to the Oracle Cloud label", () => {
// The provider is called `oraclecloud` in the wizard but the docs path
// uses the `oci` slug, so the parser must know both refer to the same
// provider.
const destination = getProviderWizardDocsDestination(
"https://docs.prowler.com/user-guide/providers/oci/authentication",
);
expect(destination).toBe("Oracle Cloud");
});
it("maps the microsoft365 docs slug to the Microsoft 365 label", () => {
// Same shape: the wizard's `m365` maps to the `microsoft365` folder in
// docs, so the parser must recognise the folder slug as the provider.
const destination = getProviderWizardDocsDestination(
"https://docs.prowler.com/user-guide/providers/microsoft365/authentication",
);
expect(destination).toBe("Microsoft 365");
});
it("returns a compact destination label for long docs links", () => {
const destination = getProviderWizardDocsDestination(
"https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations",
@@ -69,3 +179,101 @@ describe("getProviderWizardDocsDestination", () => {
expect(destination).toBe("AWS Organizations");
});
});
// The URL maps in `ui/lib/external-urls.ts` and the `destinationLabelMap` in
// this file share the same set of providers but use different keys (wizard
// slug vs. docs path slug — e.g. `oraclecloud` vs `oci`, `m365` vs
// `microsoft365`). Nothing structural stops a provider from being added to
// one file and forgotten in the other, which would silently render the
// title-cased URL segment (e.g. "Getting Started Aws") in the modal header
// instead of the intended label.
//
// This suite locks the end-to-end contract: for every provider in
// `PROVIDER_TYPES`, on every wizard step, the URL emitted by
// `getProviderHelpText` must round-trip through the parser into the expected
// label. `Record<KnownProviderType, ...>` also gives compile-time coverage —
// adding a provider to `PROVIDER_TYPES` requires updating this table.
const EXPECTED_MODAL_HEADER_LABEL: Record<KnownProviderType, string> = {
aws: "AWS",
azure: "Azure",
m365: "Microsoft 365",
gcp: "GCP",
kubernetes: "Kubernetes",
github: "GitHub",
iac: "IaC",
image: "Image",
oraclecloud: "Oracle Cloud",
mongodbatlas: "MongoDB Atlas",
alibabacloud: "Alibaba Cloud",
cloudflare: "Cloudflare",
openstack: "OpenStack",
googleworkspace: "Google Workspace",
vercel: "Vercel",
okta: "Okta",
};
// Method-specific labels for the credentials step. Providers with per-method
// docs subsections (AWS, M365, Alibaba Cloud, Cloudflare) must round-trip
// through `getProviderWizardDocsDestination` into a specific label —
// otherwise the URL in `PROVIDER_CREDENTIALS_METHOD_DOCS_URL` and the label
// in `docsSectionLabelMap` have drifted apart. Providers without a per-method
// deep link (GCP, GitHub, and every single-method provider) fall through to
// the generic `EXPECTED_MODAL_HEADER_LABEL` above and are not listed here.
const EXPECTED_METHOD_MODAL_HEADER_LABEL: Array<
[KnownProviderType, string, string]
> = [
["aws", "role", "AWS Assume Role"],
["aws", "credentials", "AWS Credentials"],
["m365", "app_certificate", "M365 Certificate"],
["m365", "app_client_secret", "M365 Client Secret"],
["alibabacloud", "role", "Alibaba Cloud RAM Role"],
["alibabacloud", "credentials", "Alibaba Cloud Credentials"],
["cloudflare", "api_token", "Cloudflare API Token"],
["cloudflare", "api_key", "Cloudflare API Key"],
];
describe("provider label parity", () => {
const STEPS = [
PROVIDER_WIZARD_STEP.CONNECT,
PROVIDER_WIZARD_STEP.CREDENTIALS,
PROVIDER_WIZARD_STEP.TEST,
PROVIDER_WIZARD_STEP.LAUNCH,
] as const;
it("resolves the expected modal header label for every provider on every wizard step", () => {
for (const provider of PROVIDER_TYPES) {
for (const step of STEPS) {
const { link } = getProviderHelpText(provider, step);
const label = getProviderWizardDocsDestination(link);
expect(
label,
`Modal header label drift for provider="${provider}" step=${step}: getProviderHelpText returned "${link}" which the parser resolved to "${label}" instead of "${EXPECTED_MODAL_HEADER_LABEL[provider]}". Check destinationLabelMap in provider-wizard-modal.utils.ts.`,
).toBe(EXPECTED_MODAL_HEADER_LABEL[provider]);
}
}
});
it("resolves the expected modal header label for every method-specific credentials deep link", () => {
// Round-trip check: catches drift between the URLs in
// `PROVIDER_CREDENTIALS_METHOD_DOCS_URL` (external-urls.ts) and the
// section labels in `docsSectionLabelMap` (this file). A hash typo on
// either side would otherwise slip past the individual literal-URL tests
// in both files.
for (const [
provider,
method,
expected,
] of EXPECTED_METHOD_MODAL_HEADER_LABEL) {
const { link } = getProviderHelpText(
provider,
PROVIDER_WIZARD_STEP.CREDENTIALS,
method,
);
const label = getProviderWizardDocsDestination(link);
expect(
label,
`Method-specific label drift for provider="${provider}" method="${method}": getProviderHelpText returned "${link}" which the parser resolved to "${label}" instead of "${expected}". Ensure PROVIDER_CREDENTIALS_METHOD_DOCS_URL and docsSectionLabelMap agree on the anchor.`,
).toBe(expected);
}
});
});
@@ -35,21 +35,55 @@ export function getProviderWizardDocsDestination(docsLink: string) {
aws: "AWS",
azure: "Azure",
m365: "Microsoft 365",
microsoft365: "Microsoft 365",
gcp: "GCP",
k8s: "Kubernetes",
kubernetes: "Kubernetes",
github: "GitHub",
iac: "IaC",
image: "Image",
oci: "Oracle Cloud",
oraclecloud: "Oracle Cloud",
mongodbatlas: "MongoDB Atlas",
alibabacloud: "Alibaba Cloud",
cloudflare: "Cloudflare",
openstack: "OpenStack",
googleworkspace: "Google Workspace",
vercel: "Vercel",
okta: "Okta",
help: "Provider",
providers: "Provider",
};
const stripUrlShapePrefix = (segment: string) =>
segment
.replace(/^getting-started-/, "")
.replace(/^provider-/, "")
.replace(/^prowler-cloud-/, "");
try {
const parsed = new URL(docsLink);
// Labels for method-specific credentials-step deep links. Keyed by the
// docs URL slug (which can differ from the wizard provider key — e.g.
// the docs use `microsoft365` while the wizard uses `m365`). Providers
// whose credentials-step URL is the general step anchor are omitted
// here and fall back to the provider label ("AWS", "Google Workspace",
// etc.) via the `destinationLabelMap` below.
const docsSectionLabelMap: Record<string, string> = {
"aws#assume-role-recommended": "AWS Assume Role",
"aws#credentials-static-access-keys": "AWS Credentials",
"microsoft365#application-certificate-authentication-recommended":
"M365 Certificate",
"microsoft365#application-client-secret-authentication":
"M365 Client Secret",
"alibabacloud#ram-role-assumption-recommended": "Alibaba Cloud RAM Role",
"alibabacloud#credentials-static-access-keys":
"Alibaba Cloud Credentials",
"cloudflare#user-api-token-authentication-recommended":
"Cloudflare API Token",
"cloudflare#api-key-and-email-authentication-legacy":
"Cloudflare API Key",
};
const pathSegments = parsed.pathname
.split("/")
.filter((segment) => segment.length > 0);
@@ -59,16 +93,32 @@ export function getProviderWizardDocsDestination(docsLink: string) {
return parsed.hostname;
}
const compactDestination = lastSegment
.replace(/^provider-/, "")
.replace(/^prowler-cloud-/, "");
const mappedDestination = destinationLabelMap[compactDestination];
// For docs URLs shaped as `/user-guide/providers/<slug>/<page>` the
// provider slug is the segment right after `providers`, not the last one
// (which is a page name like `authentication` or `getting-started-<X>`).
// Prefer that when present so pages like
// `/user-guide/providers/aws/authentication` map to "AWS" instead of
// the meaningless title-cased fallback ("Authentication").
const providersIndex = pathSegments.indexOf("providers");
const providerSlugFromPath =
providersIndex >= 0 && providersIndex + 1 < pathSegments.length
? pathSegments[providersIndex + 1]
: undefined;
if (mappedDestination) {
return mappedDestination;
if (providerSlugFromPath && parsed.hash) {
const sectionLabel =
docsSectionLabelMap[`${providerSlugFromPath}${parsed.hash}`];
if (sectionLabel) return sectionLabel;
}
return compactDestination
for (const candidate of [providerSlugFromPath, lastSegment]) {
if (!candidate) continue;
const compact = stripUrlShapePrefix(candidate);
const mapped = destinationLabelMap[compact];
if (mapped) return mapped;
}
return stripUrlShapePrefix(lastSegment)
.split("-")
.map((word) =>
word.length === 0 ? word : word[0].toUpperCase() + word.slice(1),
+246
View File
@@ -3,9 +3,12 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { PROVIDER_WIZARD_STEP } from "@/types/provider-wizard";
import {
getAWSCredentialsTemplateLinks,
getAWSOrgDeploymentQuickLink,
getProviderHelpText,
PROWLER_CF_TEMPLATE_URL,
} from "./external-urls";
@@ -107,6 +110,249 @@ describe("getAWSOrgDeploymentQuickLink", () => {
});
});
describe("getProviderHelpText", () => {
const AWS_SHORTLINK = "https://goto.prowler.com/provider-aws";
const AWS_CREDENTIALS_STEP_DOCS =
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#step-3-set-up-aws-authentication";
it("returns the provider shortlink on the connect step", () => {
// Given the user is picking a provider (no deep-link into auth yet)
// When
const { link } = getProviderHelpText("aws", PROVIDER_WIZARD_STEP.CONNECT);
// Then
expect(link).toBe(AWS_SHORTLINK);
});
it("points to the credentials section of the getting-started page on the credentials step", () => {
// No method picked yet — link should scroll the getting-started page to
// the credentials/authentication step so the user reads about the choice
// in the same page they came from.
const { link } = getProviderHelpText(
"aws",
PROVIDER_WIZARD_STEP.CREDENTIALS,
);
expect(link).toBe(AWS_CREDENTIALS_STEP_DOCS);
});
it("points AWS assume role credentials to the exact setup section", () => {
const { link } = getProviderHelpText(
"aws",
PROVIDER_WIZARD_STEP.CREDENTIALS,
"role",
);
expect(link).toBe(
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#assume-role-recommended",
);
});
it("points AWS static credentials to the exact setup section", () => {
const { link } = getProviderHelpText(
"aws",
PROVIDER_WIZARD_STEP.CREDENTIALS,
"credentials",
);
expect(link).toBe(
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#credentials-static-access-keys",
);
});
it("falls back to the credentials step section when the picked method has no dedicated subsection", () => {
// GCP's methods render inside a Mintlify <Tabs> component in the docs
// page, so no per-method anchor exists. Any method-selected variant
// resolves to the general credentials step anchor.
const { link } = getProviderHelpText(
"gcp",
PROVIDER_WIZARD_STEP.CREDENTIALS,
"service-account",
);
expect(link).toBe(
"https://docs.prowler.com/user-guide/providers/gcp/getting-started-gcp#step-3-set-up-gcp-authentication",
);
});
it("keeps the shortlink on the test connection step", () => {
// Credentials-step docs are only surfaced while the user is still
// supplying credentials; after that the shortlink landing is the useful
// destination.
const { link } = getProviderHelpText("aws", PROVIDER_WIZARD_STEP.TEST);
expect(link).toBe(AWS_SHORTLINK);
});
it("keeps the shortlink on the launch step", () => {
const { link } = getProviderHelpText("aws", PROVIDER_WIZARD_STEP.LAUNCH);
expect(link).toBe(AWS_SHORTLINK);
});
it("resolves the credentials-step link for every supported provider", () => {
// Guard against silently dropping a provider from
// PROVIDER_CREDENTIALS_STEP_DOCS_URL. When no auth method is selected
// yet, every provider should deep-link to its own getting-started
// credentials section (never to authentication.mdx).
const cases: Array<[string, string]> = [
[
"aws",
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#step-3-set-up-aws-authentication",
],
[
"azure",
"https://docs.prowler.com/user-guide/providers/azure/getting-started-azure#step-3-add-credentials-to-prowler-cloud",
],
[
"m365",
"https://docs.prowler.com/user-guide/providers/microsoft365/getting-started-m365#step-3-choose-and-provide-authentication",
],
[
"gcp",
"https://docs.prowler.com/user-guide/providers/gcp/getting-started-gcp#step-3-set-up-gcp-authentication",
],
[
"kubernetes",
"https://docs.prowler.com/user-guide/providers/kubernetes/getting-started-k8s#step-2-configure-kubernetes-authentication",
],
[
"github",
"https://docs.prowler.com/user-guide/providers/github/getting-started-github#step-3-choose-authentication-method",
],
[
"iac",
"https://docs.prowler.com/user-guide/providers/iac/getting-started-iac#step-2-enter-authentication-details",
],
[
"image",
"https://docs.prowler.com/user-guide/providers/image/getting-started-image#step-2-enter-authentication-and-scan-filters",
],
[
"oraclecloud",
"https://docs.prowler.com/user-guide/providers/oci/getting-started-oci#step-3-add-oci-api-key-credentials",
],
[
"mongodbatlas",
"https://docs.prowler.com/user-guide/providers/mongodbatlas/getting-started-mongodbatlas#step-2-provide-api-credentials",
],
[
"alibabacloud",
"https://docs.prowler.com/user-guide/providers/alibabacloud/getting-started-alibabacloud#step-3-choose-and-provide-authentication",
],
[
"cloudflare",
"https://docs.prowler.com/user-guide/providers/cloudflare/getting-started-cloudflare#step-3-choose-and-provide-authentication",
],
[
"openstack",
"https://docs.prowler.com/user-guide/providers/openstack/getting-started-openstack#step-2-provide-credentials",
],
[
"googleworkspace",
"https://docs.prowler.com/user-guide/providers/googleworkspace/getting-started-googleworkspace#step-3-provide-credentials",
],
[
"vercel",
"https://docs.prowler.com/user-guide/providers/vercel/getting-started-vercel#step-2-provide-credentials",
],
[
"okta",
"https://docs.prowler.com/user-guide/providers/okta/getting-started-okta#step-2-provide-credentials",
],
];
for (const [provider, expected] of cases) {
expect(
getProviderHelpText(provider, PROVIDER_WIZARD_STEP.CREDENTIALS).link,
).toBe(expected);
}
});
it("resolves the method-specific credentials link for every provider with a per-method subsection", () => {
// Providers whose docs have a heading per auth method: verify each
// (provider, method) combo maps to the exact subsection anchor. Missing
// an entry in PROVIDER_CREDENTIALS_METHOD_DOCS_URL silently regresses
// the user to the general step section — this test catches that.
const cases: Array<[string, string, string]> = [
[
"aws",
"role",
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#assume-role-recommended",
],
[
"aws",
"credentials",
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#credentials-static-access-keys",
],
[
"m365",
"app_certificate",
"https://docs.prowler.com/user-guide/providers/microsoft365/getting-started-m365#application-certificate-authentication-recommended",
],
[
"m365",
"app_client_secret",
"https://docs.prowler.com/user-guide/providers/microsoft365/getting-started-m365#application-client-secret-authentication",
],
[
"alibabacloud",
"role",
"https://docs.prowler.com/user-guide/providers/alibabacloud/getting-started-alibabacloud#ram-role-assumption-recommended",
],
[
"alibabacloud",
"credentials",
"https://docs.prowler.com/user-guide/providers/alibabacloud/getting-started-alibabacloud#credentials-static-access-keys",
],
[
"cloudflare",
"api_token",
"https://docs.prowler.com/user-guide/providers/cloudflare/getting-started-cloudflare#user-api-token-authentication-recommended",
],
[
"cloudflare",
"api_key",
"https://docs.prowler.com/user-guide/providers/cloudflare/getting-started-cloudflare#api-key-and-email-authentication-legacy",
],
];
for (const [provider, method, expected] of cases) {
expect(
getProviderHelpText(provider, PROVIDER_WIZARD_STEP.CREDENTIALS, method)
.link,
).toBe(expected);
}
});
it("falls back to the generic help shortlink for unknown providers regardless of step", () => {
// Unknown providers have no dedicated docs page, so a step-specific
// anchor would deep-link into nothing.
const { link } = getProviderHelpText(
"not-a-real-provider",
PROVIDER_WIZARD_STEP.CREDENTIALS,
);
expect(link).toBe("https://goto.prowler.com/provider-help");
});
it("falls back for unknown providers colliding with Object.prototype", () => {
// Given
const providers = ["constructor", "toString", "__proto__"];
for (const provider of providers) {
// When
const { link } = getProviderHelpText(
provider,
PROVIDER_WIZARD_STEP.CREDENTIALS,
);
// Then
expect(link).toBe("https://goto.prowler.com/provider-help");
}
});
});
describe("Prowler CloudFormation template", () => {
it("should define every parameter used by the UI quick-create links", () => {
// Given
+173 -87
View File
@@ -1,4 +1,8 @@
import type { IntegrationType } from "../types/integrations";
import {
PROVIDER_WIZARD_STEP,
type ProviderWizardStep,
} from "../types/provider-wizard";
// Documentation URLs
export const DOCS_URLS = {
@@ -60,94 +64,176 @@ const buildCloudFormationQuickCreateLink = (
return `${CF_QUICKCREATE_BASE_URL}?${searchParams.toString()}`;
};
export const getProviderHelpText = (provider: string) => {
switch (provider) {
case "aws":
return {
text: "Need help connecting your AWS account?",
link: "https://goto.prowler.com/provider-aws",
};
case "azure":
return {
text: "Need help connecting your Azure subscription?",
link: "https://goto.prowler.com/provider-azure",
};
case "m365":
return {
text: "Need help connecting your Microsoft 365 account?",
link: "https://goto.prowler.com/provider-m365",
};
case "gcp":
return {
text: "Need help connecting your GCP project?",
link: "https://goto.prowler.com/provider-gcp",
};
case "kubernetes":
return {
text: "Need help connecting your Kubernetes cluster?",
link: "https://goto.prowler.com/provider-k8s",
};
case "github":
return {
text: "Need help connecting your GitHub account?",
link: "https://goto.prowler.com/provider-github",
};
case "iac":
return {
text: "Need help scanning your Infrastructure as Code repository?",
link: "https://goto.prowler.com/provider-iac",
};
case "image":
return {
text: "Need help scanning your container registry?",
link: "https://goto.prowler.com/provider-image",
};
case "oraclecloud":
return {
text: "Need help connecting your Oracle Cloud account?",
link: "https://goto.prowler.com/provider-oraclecloud",
};
case "mongodbatlas":
return {
text: "Need help connecting your MongoDB Atlas organization?",
link: "https://goto.prowler.com/provider-mongodbatlas",
};
case "alibabacloud":
return {
text: "Need help connecting your Alibaba Cloud account?",
link: "https://goto.prowler.com/provider-alibabacloud",
};
case "cloudflare":
return {
text: "Need help connecting your Cloudflare account?",
link: "https://goto.prowler.com/provider-cloudflare",
};
case "openstack":
return {
text: "Need help connecting your OpenStack cloud?",
link: "https://goto.prowler.com/provider-openstack",
};
case "googleworkspace":
return {
text: "Need help connecting your Google Workspace account?",
link: "https://goto.prowler.com/provider-googleworkspace",
};
case "vercel":
return {
text: "Need help connecting your Vercel team?",
link: "https://goto.prowler.com/provider-vercel",
};
case "okta":
return {
text: "Need help connecting your Okta organization?",
link: "https://goto.prowler.com/provider-okta",
};
default:
return {
text: "How to setup a provider?",
link: "https://goto.prowler.com/provider-help",
};
// Shortlinks are used for all wizard steps except credentials so link
// ownership stays with the docs/marketing team: they can retarget
// destinations from the shortener panel without a UI PR.
const PROVIDER_DOCS_SHORTLINK: Record<string, string> = {
aws: "https://goto.prowler.com/provider-aws",
azure: "https://goto.prowler.com/provider-azure",
m365: "https://goto.prowler.com/provider-m365",
gcp: "https://goto.prowler.com/provider-gcp",
kubernetes: "https://goto.prowler.com/provider-k8s",
github: "https://goto.prowler.com/provider-github",
iac: "https://goto.prowler.com/provider-iac",
image: "https://goto.prowler.com/provider-image",
oraclecloud: "https://goto.prowler.com/provider-oraclecloud",
mongodbatlas: "https://goto.prowler.com/provider-mongodbatlas",
alibabacloud: "https://goto.prowler.com/provider-alibabacloud",
cloudflare: "https://goto.prowler.com/provider-cloudflare",
openstack: "https://goto.prowler.com/provider-openstack",
googleworkspace: "https://goto.prowler.com/provider-googleworkspace",
vercel: "https://goto.prowler.com/provider-vercel",
okta: "https://goto.prowler.com/provider-okta",
};
// Default target for the credentials step: the section of the provider's
// getting-started page that introduces the credentials flow. The getting-
// started page keeps the user in the same mental model as the wizard, and
// each section already links to `authentication.mdx` for readers who need
// deeper detail. That indirection is intentional — we do NOT deep-link into
// `authentication.mdx` from the wizard, otherwise the user is jumped into
// low-level docs before they have the context to make sense of them.
const PROVIDER_CREDENTIALS_STEP_DOCS_URL: Record<string, string> = {
aws: "https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#step-3-set-up-aws-authentication",
azure:
"https://docs.prowler.com/user-guide/providers/azure/getting-started-azure#step-3-add-credentials-to-prowler-cloud",
m365: "https://docs.prowler.com/user-guide/providers/microsoft365/getting-started-m365#step-3-choose-and-provide-authentication",
gcp: "https://docs.prowler.com/user-guide/providers/gcp/getting-started-gcp#step-3-set-up-gcp-authentication",
kubernetes:
"https://docs.prowler.com/user-guide/providers/kubernetes/getting-started-k8s#step-2-configure-kubernetes-authentication",
github:
"https://docs.prowler.com/user-guide/providers/github/getting-started-github#step-3-choose-authentication-method",
iac: "https://docs.prowler.com/user-guide/providers/iac/getting-started-iac#step-2-enter-authentication-details",
image:
"https://docs.prowler.com/user-guide/providers/image/getting-started-image#step-2-enter-authentication-and-scan-filters",
oraclecloud:
"https://docs.prowler.com/user-guide/providers/oci/getting-started-oci#step-3-add-oci-api-key-credentials",
mongodbatlas:
"https://docs.prowler.com/user-guide/providers/mongodbatlas/getting-started-mongodbatlas#step-2-provide-api-credentials",
alibabacloud:
"https://docs.prowler.com/user-guide/providers/alibabacloud/getting-started-alibabacloud#step-3-choose-and-provide-authentication",
cloudflare:
"https://docs.prowler.com/user-guide/providers/cloudflare/getting-started-cloudflare#step-3-choose-and-provide-authentication",
openstack:
"https://docs.prowler.com/user-guide/providers/openstack/getting-started-openstack#step-2-provide-credentials",
googleworkspace:
"https://docs.prowler.com/user-guide/providers/googleworkspace/getting-started-googleworkspace#step-3-provide-credentials",
vercel:
"https://docs.prowler.com/user-guide/providers/vercel/getting-started-vercel#step-2-provide-credentials",
okta: "https://docs.prowler.com/user-guide/providers/okta/getting-started-okta#step-2-provide-credentials",
};
// When the user has picked a specific auth method inside the credentials
// step, jump directly to that method's subsection in the getting-started
// page. Only providers whose docs have a heading per method are listed —
// GCP and GitHub render their methods inside a Mintlify `<Tabs>` component
// so a per-method anchor isn't available today; they fall back to the
// general step URL above.
const PROVIDER_CREDENTIALS_METHOD_DOCS_URL: Record<
string,
Record<string, string>
> = {
aws: {
role: "https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#assume-role-recommended",
credentials:
"https://docs.prowler.com/user-guide/providers/aws/getting-started-aws#credentials-static-access-keys",
},
m365: {
app_certificate:
"https://docs.prowler.com/user-guide/providers/microsoft365/getting-started-m365#application-certificate-authentication-recommended",
app_client_secret:
"https://docs.prowler.com/user-guide/providers/microsoft365/getting-started-m365#application-client-secret-authentication",
},
alibabacloud: {
role: "https://docs.prowler.com/user-guide/providers/alibabacloud/getting-started-alibabacloud#ram-role-assumption-recommended",
credentials:
"https://docs.prowler.com/user-guide/providers/alibabacloud/getting-started-alibabacloud#credentials-static-access-keys",
},
cloudflare: {
api_token:
"https://docs.prowler.com/user-guide/providers/cloudflare/getting-started-cloudflare#user-api-token-authentication-recommended",
api_key:
"https://docs.prowler.com/user-guide/providers/cloudflare/getting-started-cloudflare#api-key-and-email-authentication-legacy",
},
};
const PROVIDER_HELP_FALLBACK_URL = "https://goto.prowler.com/provider-help";
const getOwnRecordValue = <T>(
record: Readonly<Record<string, T>>,
key: string,
): T | undefined => (Object.hasOwn(record, key) ? record[key] : undefined);
const resolveDocsLink = (
provider: string,
step?: ProviderWizardStep,
credentialsMethod?: string | null,
) => {
const shortlink = getOwnRecordValue(PROVIDER_DOCS_SHORTLINK, provider);
if (step === PROVIDER_WIZARD_STEP.CREDENTIALS) {
if (credentialsMethod) {
const methodDocs = getOwnRecordValue(
PROVIDER_CREDENTIALS_METHOD_DOCS_URL,
provider,
);
const methodUrl = methodDocs
? getOwnRecordValue(methodDocs, credentialsMethod)
: undefined;
if (methodUrl) return methodUrl;
}
const stepUrl = getOwnRecordValue(
PROVIDER_CREDENTIALS_STEP_DOCS_URL,
provider,
);
if (stepUrl) return stepUrl;
}
return shortlink;
};
const PROVIDER_HELP_TEXT: Record<string, string> = {
aws: "Need help connecting your AWS account?",
azure: "Need help connecting your Azure subscription?",
m365: "Need help connecting your Microsoft 365 account?",
gcp: "Need help connecting your GCP project?",
kubernetes: "Need help connecting your Kubernetes cluster?",
github: "Need help connecting your GitHub account?",
iac: "Need help scanning your Infrastructure as Code repository?",
image: "Need help scanning your container registry?",
oraclecloud: "Need help connecting your Oracle Cloud account?",
mongodbatlas: "Need help connecting your MongoDB Atlas organization?",
alibabacloud: "Need help connecting your Alibaba Cloud account?",
cloudflare: "Need help connecting your Cloudflare account?",
openstack: "Need help connecting your OpenStack cloud?",
googleworkspace: "Need help connecting your Google Workspace account?",
vercel: "Need help connecting your Vercel team?",
okta: "Need help connecting your Okta organization?",
};
export const getProviderHelpText = (
provider: string,
step?: ProviderWizardStep,
credentialsMethod?: string | null,
) => {
const link = resolveDocsLink(provider, step, credentialsMethod);
if (!link) {
// Unknown provider: hand off to the generic help shortlink instead of
// deep-linking into a page that may not exist.
return {
text: "How to setup a provider?",
link: PROVIDER_HELP_FALLBACK_URL,
};
}
return {
text:
getOwnRecordValue(PROVIDER_HELP_TEXT, provider) ??
"Need help connecting your provider?",
link,
};
};
export const getAWSCredentialsTemplateLinks = (