mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-05 03:12:14 +00:00
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:
co-authored by
Daniel Barranquero
alejandrobailo
parent
64c0cf900f
commit
fb9d989be8
@@ -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),
|
||||
|
||||
@@ -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
@@ -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 = (
|
||||
|
||||
Reference in New Issue
Block a user