From 9822fd97d76b933a8011acbdb445f92719b8da7b Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:40:21 +0200 Subject: [PATCH] feat(dashboard): redesign local dashboard sidebar and pages (#11972) --- dashboard/__main__.py | 267 +++++++----- dashboard/assets/cloud-pages.css | 389 ++++++++++++++++++ .../assets/images/icons/cloud/alerts.svg | 1 + .../images/icons/cloud/attack-paths.svg | 1 + dashboard/assets/images/icons/cloud/check.svg | 1 + .../assets/images/icons/cloud/compliance.svg | 4 + dashboard/assets/images/icons/cloud/docs.svg | 1 + .../assets/images/icons/cloud/findings.svg | 1 + dashboard/assets/images/icons/cloud/help.svg | 1 + .../images/icons/cloud/integrations.svg | 1 + .../images/icons/cloud/lighthouse-ai.svg | 1 + .../assets/images/icons/cloud/mutelist.svg | 1 + .../images/icons/cloud/organization.svg | 1 + .../assets/images/icons/cloud/overview.svg | 4 + .../images/icons/cloud/prowler-lockup.svg | 1 + .../images/icons/cloud/prowler-mark.svg | 1 + dashboard/pages/cloud.py | 263 ++++++++++++ .../dashboard-local-redesign.changed.md | 1 + 18 files changed, 826 insertions(+), 114 deletions(-) create mode 100644 dashboard/assets/cloud-pages.css create mode 100644 dashboard/assets/images/icons/cloud/alerts.svg create mode 100644 dashboard/assets/images/icons/cloud/attack-paths.svg create mode 100644 dashboard/assets/images/icons/cloud/check.svg create mode 100644 dashboard/assets/images/icons/cloud/compliance.svg create mode 100644 dashboard/assets/images/icons/cloud/docs.svg create mode 100644 dashboard/assets/images/icons/cloud/findings.svg create mode 100644 dashboard/assets/images/icons/cloud/help.svg create mode 100644 dashboard/assets/images/icons/cloud/integrations.svg create mode 100644 dashboard/assets/images/icons/cloud/lighthouse-ai.svg create mode 100644 dashboard/assets/images/icons/cloud/mutelist.svg create mode 100644 dashboard/assets/images/icons/cloud/organization.svg create mode 100644 dashboard/assets/images/icons/cloud/overview.svg create mode 100644 dashboard/assets/images/icons/cloud/prowler-lockup.svg create mode 100644 dashboard/assets/images/icons/cloud/prowler-mark.svg create mode 100644 dashboard/pages/cloud.py create mode 100644 prowler/changelog.d/dashboard-local-redesign.changed.md diff --git a/dashboard/__main__.py b/dashboard/__main__.py index 664ce3bfa5..6a36bda7ed 100644 --- a/dashboard/__main__.py +++ b/dashboard/__main__.py @@ -20,7 +20,7 @@ print_banner() print( f"{Fore.GREEN}Loading all CSV files from the folder {folder_path_overview} ...\n{Style.RESET_ALL}" ) -cli.show_server_banner = lambda *x: click.echo( +cli.show_server_banner = lambda *_: click.echo( f"{Fore.YELLOW}NOTE:{Style.RESET_ALL} If you are using {Fore.GREEN}{Style.BRIGHT}Prowler Cloud{Style.RESET_ALL} with the S3 integration or that integration \nfrom {Fore.CYAN}{Style.BRIGHT}Prowler CLI{Style.RESET_ALL} and you want to use your data from your S3 bucket,\nrun: `{orange_color}aws s3 cp s3:///output/csv ./output --recursive{Style.RESET_ALL}`\nand then run `prowler dashboard` again to load the new files." ) @@ -33,148 +33,187 @@ dashboard = dash.Dash( title="Prowler Dashboard", ) -# Logo -prowler_logo = html.Img( - src="https://cdn.prod.website-files.com/68c4ec3f9fb7b154fbcb6e36/68ffb46d40ed7faa37a592a5_prowler-logo.png", - alt="Prowler Logo", +# ``use_pages`` above already imported dashboard/pages/cloud.py and registered +# every /cloud/* route. Import its metadata now (after app instantiation) so +# the sidebar and the gated pages share a single source of truth. +from dashboard.pages.cloud import CLOUD_FEATURES_BY_SLUG # noqa: E402 + +ICON_DIR = "/assets/images/icons/cloud" + +# Official marketing "PROWLER / LOCAL DASHBOARD" lockup (white wordmark + teal +# gradient sublabel) shown in the expanded sidebar. Vector SVG so it stays crisp +# at any DPI. The sublabel is right-anchored (text-anchor="end"), so a font +# fallback widens it leftward rather than clipping at the edge. +prowler_lockup = html.Img( + src=f"{ICON_DIR}/prowler-lockup.svg", + alt="Prowler Local Dashboard", + className="pc-brand-lockup", ) -menu_icons = { - "overview": "/assets/images/icons/overview.svg", - "compliance": "/assets/images/icons/compliance.svg", -} +# Compact brand mark shown only when the sidebar collapses to its icon rail. +prowler_mark = html.Img( + src=f"{ICON_DIR}/prowler-mark.svg", + alt="Prowler", + className="pc-brand-mark", +) + +# Locally functional destinations (Overview + Compliance). +DASHBOARD_ITEMS = [ + {"label": "Overview", "route": "/", "icon": f"{ICON_DIR}/overview.svg"}, + { + "label": "Compliance", + "route": "/compliance", + "icon": f"{ICON_DIR}/compliance.svg", + }, +] + +# Gated navigation groups reference the shared feature metadata by slug so the +# sidebar and the informational pages never drift apart. +GATED_GROUPS = [ + ("Upgrade to Prowler Cloud", ["lighthouse-ai", "attack-paths", "findings"]), + ("Configuration", ["alerts", "mutelist", "integrations"]), + ("Workspace", ["organization"]), +] + +HELP_LINKS = [ + { + "title": "Help", + "url": "https://github.com/prowler-cloud/prowler/issues", + "icon": f"{ICON_DIR}/help.svg", + }, + { + "title": "Docs", + "url": "https://docs.prowler.com", + "icon": f"{ICON_DIR}/docs.svg", + }, +] -# Function to generate navigation links -def generate_nav_links(current_path): - nav_links = [] - for page in dash.page_registry.values(): - # Gets the icon URL based on the page name - icon_url = menu_icons.get(page["name"].lower()) - is_active = ( - " bg-prowler-stone-950 border-r-4 border-solid border-prowler-lime" - if current_path == page["relative_path"] - else "" - ) - link_class = f"block hover:bg-prowler-stone-950 hover:border-r-4 hover:border-solid hover:border-prowler-lime{is_active}" +def _mask_style(icon_url): + """Inline style rendering a recolorable mask icon from a local asset.""" + return { + "WebkitMaskImage": f"url({icon_url})", + "maskImage": f"url({icon_url})", + } - link_content = html.Span( + +def _nav_icon(icon_url): + return html.Span(className="pc-ico", style=_mask_style(icon_url)) + + +def _nav_item(label, route, icon_url, current_path, gated=False): + is_active = current_path == route + class_name = "pc-nav-item pc-active" if is_active else "pc-nav-item" + + content = [ + _nav_icon(icon_url), + html.Span(label, className="pc-nav-label"), + ] + if gated: + content.append(html.Span("Prowler Cloud", className="pc-pill")) + + return dcc.Link(content, href=route, className=class_name) + + +def _section_label(title): + return html.Div(title, className="pc-section") + + +def generate_sidebar(current_path): + children = [ + # Brand lockup: full wordmark when expanded, compact mark when collapsed. + html.Div( + [prowler_lockup, prowler_mark], + className="pc-brand", + ), + # Dashboards section — the only locally functional destinations. + _section_label("Dashboards"), + html.Nav( [ - html.Img(src=icon_url, className="w-5"), - html.Span( - page["name"], className="font-medium text-base leading-6 text-white" - ), + _nav_item(item["label"], item["route"], item["icon"], current_path) + for item in DASHBOARD_ITEMS ], - className="flex justify-center lg:justify-normal items-center gap-x-3 py-2 px-3", - ) - - nav_link = html.Li( - dcc.Link(link_content, href=page["relative_path"], className=link_class) - ) - nav_links.append(nav_link) - return nav_links - - -def generate_help_menu(): - help_links = [ - { - "title": "Help", - "url": "https://github.com/prowler-cloud/prowler/issues", - "icon": "/assets/images/icons/help.png", - }, - { - "title": "Docs", - "url": "https://docs.prowler.com", - "icon": "/assets/images/icons/docs.png", - }, + className="pc-nav", + ), ] - link_class = "block hover:bg-prowler-stone-950 hover:border-r-4 hover:border-solid hover:border-prowler-lime" - - menu_items = [] - for link in help_links: - menu_item = html.Li( - html.A( - html.Span( - [ - html.Img(src=link["icon"], className="w-5"), - html.Span( - link["title"], - className="font-medium text-base leading-6 text-white", - ), - ], - className="flex items-center gap-x-3 py-2 px-3", - ), - href=link["url"], - target="_blank", - className=link_class, + # Gated groups (Prowler Cloud only). + for section_title, slugs in GATED_GROUPS: + children.append(_section_label(section_title)) + children.append( + html.Nav( + [ + _nav_item( + CLOUD_FEATURES_BY_SLUG[slug]["nav_label"], + CLOUD_FEATURES_BY_SLUG[slug]["route"], + CLOUD_FEATURES_BY_SLUG[slug]["icon"], + current_path, + gated=True, + ) + for slug in slugs + ], + className="pc-nav", ) ) - menu_items.append(menu_item) - return menu_items + # Help and Docs pinned to the bottom, separated by a neutral top border. + children.append( + html.Nav( + [ + html.A( + [ + _nav_icon(link["icon"]), + html.Span(link["title"], className="pc-nav-label"), + ], + href=link["url"], + target="_blank", + rel="noopener noreferrer", + className="pc-nav-item", + ) + for link in HELP_LINKS + ], + className="pc-nav pc-footer", + ) + ) + + return html.Div(children, className="pc-sidebar pc-font") # Layout dashboard.layout = html.Div( [ - dcc.Location(id="url", refresh=False), html.Link(rel="icon", href="assets/favicon.ico"), - # Placeholder for dynamic navigation bar html.Div( [ + # Dynamic sidebar (rebuilt on navigation for active state). + html.Div(id="navigation-bar"), + # Main pane hosting the routed page content. html.Div( - id="navigation-bar", className="bg-prowler-stone-900 min-w-36 z-10" - ), - html.Div( - [ + html.Div( dash.page_container, - ], + className="pc-main-inner", + ), id="content_select", - className="bg-prowler-white w-full col-span-11 h-screen mx-auto overflow-y-scroll no-scrollbar px-10 py-7", + className="pc-main pc-font no-scrollbar", ), ], - className="grid custom-grid 2xl:custom-grid-large h-screen", + className="pc-shell", ), ], className="h-screen mx-auto", ) -# Callback to update navigation bar -@dashboard.callback(Output("navigation-bar", "children"), [Input("url", "pathname")]) +# Callback to update navigation bar. +# +# Triggered off Dash Pages' own location (``_pages_location``) rather than a +# separate ``dcc.Location``. A standalone ``dcc.Location(id="url")`` stops +# emitting ``pathname`` when navigating between two pages that render an +# identical component tree — every ``/cloud/*`` gated page shares the same +# ``build_cloud_layout`` structure — which left the active highlight stuck on +# the first gated page visited. ``_pages_location`` fires on every route change. +@dashboard.callback( + Output("navigation-bar", "children"), [Input("_pages_location", "pathname")] +) def update_nav_bar(pathname): - return html.Div( - [ - html.Div([prowler_logo], className="mb-8 px-3"), - html.H6( - "Dashboards", - className="px-3 text-prowler-stone-500 text-sm opacity-90 font-regular mb-2", - ), - html.Nav( - [html.Ul(generate_nav_links(pathname), className="")], - className="flex flex-col gap-y-6", - ), - html.Nav( - [ - html.A( - [ - html.Span( - [ - html.Img(src="assets/favicon.ico", className="w-5"), - "Subscribe to Prowler Cloud", - ], - className="flex items-center gap-x-3 text-white", - ), - ], - href="https://prowler.com/", - target="_blank", - className="block p-3 uppercase text-xs hover:bg-prowler-stone-950 hover:border-r-4 hover:border-solid hover:border-prowler-lime", - ), - html.Ul(generate_help_menu(), className=""), - ], - className="flex flex-col gap-y-6 mt-auto", - ), - ], - className="flex flex-col bg-prowler-stone-900 py-7 h-full", - ) + return generate_sidebar(pathname) diff --git a/dashboard/assets/cloud-pages.css b/dashboard/assets/cloud-pages.css new file mode 100644 index 0000000000..53052eabda --- /dev/null +++ b/dashboard/assets/cloud-pages.css @@ -0,0 +1,389 @@ +/* + * Prowler Local Dashboard — Cloud upsell chrome & gated pages. + * These styles are self-contained (not dependent on the precompiled Tailwind + * bundle) so pixel specs from the PRD render reliably without a rebuild. + */ + +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); + +:root { + --pc-btn: #6ee7b7; + --pc-btn-hover: #99f6e4; + --pc-btn-press: #34d399; + --pc-grad-start: #2ee59b; + --pc-grad-end: #62dff0; + --pc-text: #020617; + --pc-text-2: #27272a; + --pc-sidebar-bg: #27272a; + --pc-pane-bg: #fdfdfd; + --pc-border: #e5e5e5; + --pc-teal-accent: #2ee59b; + --pc-sidebar-w: 264px; + --pc-sidebar-w-collapsed: 82px; +} + +.pc-font { + font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, + Helvetica, Arial, sans-serif; +} + +/* ----------------------------------------------------------------- Shell */ + +.pc-shell { + display: flex; + height: 100vh; + width: 100%; + overflow: hidden; +} + +.pc-main { + flex: 1 1 auto; + height: 100vh; + overflow-y: auto; + background: var(--pc-pane-bg); +} + +.pc-main-inner { + padding: 28px 40px 64px; +} + +/* --------------------------------------------------------------- Sidebar */ + +.pc-sidebar { + flex: 0 0 var(--pc-sidebar-w); + width: var(--pc-sidebar-w); + background: var(--pc-sidebar-bg); + height: 100vh; + display: flex; + flex-direction: column; + padding: 22px 0 16px; + overflow-y: auto; + overflow-x: hidden; +} + +.pc-sidebar::-webkit-scrollbar { + display: none; +} + +.pc-brand { + display: flex; + align-items: center; + gap: 10px; + padding: 0 18px; + margin-bottom: 22px; +} + +.pc-brand-lockup { + width: 190px; + height: auto; + display: block; +} + +/* Compact mark is only revealed on the collapsed icon rail (see media query). */ +.pc-brand-mark { + width: 30px; + height: 30px; + flex: 0 0 auto; + display: none; +} + +.pc-section { + color: #8a8a90; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.07em; + text-transform: uppercase; + padding: 0 18px; + margin: 18px 0 8px; +} + +.pc-nav { + display: flex; + flex-direction: column; + gap: 2px; +} + +.pc-nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 12px; + margin: 0 8px; + color: #ffffff; + font-size: 14px; + font-weight: 500; + text-decoration: none; + border-radius: 8px; + border-left: 3px solid transparent; + overflow: hidden; + transition: background 0.15s ease; +} + +.pc-nav-item:hover { + background: rgba(255, 255, 255, 0.07); +} + +.pc-nav-item.pc-active { + background: rgba(255, 255, 255, 0.09); + border-left-color: var(--pc-teal-accent); +} + +.pc-nav-label { + flex: 0 1 auto; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Icon rendered as a recolorable mask so one asset serves any color. */ +.pc-ico { + width: 20px; + height: 20px; + flex: 0 0 auto; + display: inline-block; + background-color: currentColor; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; +} + +.pc-pill { + flex: 0 0 auto; + margin-left: auto; + display: inline-flex; + align-items: center; + background: linear-gradient( + 112deg, + var(--pc-grad-start) 3.5%, + var(--pc-grad-end) 98.8% + ); + color: var(--pc-text); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.02em; + line-height: 1; + padding: 3px 8px; + border-radius: 9999px; + white-space: nowrap; +} + +.pc-footer { + margin-top: auto; + padding-top: 12px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +/* -------------------------------------------------------- Gated page body */ + +.pc-page { + max-width: 1120px; + margin: 0 auto; +} + +.pc-page-header { + border-bottom: 1px solid var(--pc-border); + padding-bottom: 18px; + margin-bottom: 28px; +} + +.pc-page-title-row { + display: flex; + align-items: center; + gap: 12px; +} + +/* In the page header the badge sits right next to the title (not pushed to the + far right like the sidebar pills) and is uppercased for emphasis. */ +.pc-page-title-row .pc-pill { + margin-left: 0; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.pc-page-title { + font-size: 24px; + font-weight: 700; + color: var(--pc-text); + margin: 0; +} + +.pc-page-subtitle { + font-size: 15px; + line-height: 24px; + color: #52525b; + margin: 8px 0 0; +} + +/* ----------------------------------------------------------- Upgrade card */ + +.pc-card { + position: relative; + background: #ffffff; + border: 1px solid var(--pc-border); + border-radius: 18px; + padding: 56px 40px; + overflow: hidden; +} + +.pc-card-glow { + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 560px; + height: 240px; + background: radial-gradient( + ellipse at top, + rgba(46, 229, 155, 0.2), + rgba(98, 223, 240, 0.06) 45%, + transparent 72% + ); + pointer-events: none; +} + +.pc-card-body { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.pc-feature-icon { + width: 64px; + height: 64px; + border-radius: 16px; + background: linear-gradient( + 135deg, + rgba(46, 229, 155, 0.16), + rgba(98, 223, 240, 0.14) + ); + border: 1px solid rgba(46, 229, 155, 0.3); + display: flex; + align-items: center; + justify-content: center; + color: var(--pc-text); + margin-bottom: 22px; +} + +.pc-feature-icon .pc-ico { + width: 30px; + height: 30px; +} + +.pc-avail { + color: #0e9f6e; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; + margin-bottom: 8px; +} + +.pc-card-title { + font-size: 24px; + font-weight: 700; + color: var(--pc-text); + margin: 0 0 14px; +} + +.pc-card-desc { + font-size: 15px; + line-height: 24px; + color: var(--pc-text-2); + max-width: 560px; + margin: 0 0 26px; +} + +.pc-benefits { + list-style: none; + padding: 0; + margin: 0 0 30px; + text-align: left; +} + +.pc-benefit { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 7px 0; + font-size: 15px; + line-height: 22px; + color: var(--pc-text-2); +} + +.pc-benefit-check { + flex: 0 0 auto; + width: 18px; + height: 18px; + margin-top: 2px; + color: var(--pc-btn-press); +} + +.pc-cta { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--pc-btn); + color: var(--pc-text); + font-size: 15px; + font-weight: 700; + padding: 12px 24px; + border-radius: 12px; + border: 1px solid var(--pc-btn-press); + text-decoration: none; + cursor: pointer; + transition: background 0.15s ease; +} + +.pc-cta:hover { + background: var(--pc-btn-hover); + color: var(--pc-text); +} + +.pc-cta:active { + background: var(--pc-btn-press); +} + +/* --------------------------------------------------- Responsive collapse */ + +@media (max-width: 900px) { + .pc-sidebar { + flex-basis: var(--pc-sidebar-w-collapsed); + width: var(--pc-sidebar-w-collapsed); + } + + .pc-brand { + justify-content: center; + padding: 0 8px; + } + + .pc-brand-lockup { + display: none; + } + + .pc-brand-mark { + display: block; + } + + .pc-section, + .pc-nav-label, + .pc-pill { + display: none; + } + + .pc-nav-item { + justify-content: center; + margin: 0 6px; + padding: 10px 0; + } + + .pc-main-inner { + padding: 20px 18px 48px; + } +} diff --git a/dashboard/assets/images/icons/cloud/alerts.svg b/dashboard/assets/images/icons/cloud/alerts.svg new file mode 100644 index 0000000000..6109e378ab --- /dev/null +++ b/dashboard/assets/images/icons/cloud/alerts.svg @@ -0,0 +1 @@ + diff --git a/dashboard/assets/images/icons/cloud/attack-paths.svg b/dashboard/assets/images/icons/cloud/attack-paths.svg new file mode 100644 index 0000000000..b856c6ea2f --- /dev/null +++ b/dashboard/assets/images/icons/cloud/attack-paths.svg @@ -0,0 +1 @@ + diff --git a/dashboard/assets/images/icons/cloud/check.svg b/dashboard/assets/images/icons/cloud/check.svg new file mode 100644 index 0000000000..a5f8ed0c8a --- /dev/null +++ b/dashboard/assets/images/icons/cloud/check.svg @@ -0,0 +1 @@ + diff --git a/dashboard/assets/images/icons/cloud/compliance.svg b/dashboard/assets/images/icons/cloud/compliance.svg new file mode 100644 index 0000000000..e70ebda28c --- /dev/null +++ b/dashboard/assets/images/icons/cloud/compliance.svg @@ -0,0 +1,4 @@ + diff --git a/dashboard/assets/images/icons/cloud/docs.svg b/dashboard/assets/images/icons/cloud/docs.svg new file mode 100644 index 0000000000..efc3dfc61b --- /dev/null +++ b/dashboard/assets/images/icons/cloud/docs.svg @@ -0,0 +1 @@ + diff --git a/dashboard/assets/images/icons/cloud/findings.svg b/dashboard/assets/images/icons/cloud/findings.svg new file mode 100644 index 0000000000..93fc42a516 --- /dev/null +++ b/dashboard/assets/images/icons/cloud/findings.svg @@ -0,0 +1 @@ + diff --git a/dashboard/assets/images/icons/cloud/help.svg b/dashboard/assets/images/icons/cloud/help.svg new file mode 100644 index 0000000000..2b3f1024e8 --- /dev/null +++ b/dashboard/assets/images/icons/cloud/help.svg @@ -0,0 +1 @@ + diff --git a/dashboard/assets/images/icons/cloud/integrations.svg b/dashboard/assets/images/icons/cloud/integrations.svg new file mode 100644 index 0000000000..e7a2d07603 --- /dev/null +++ b/dashboard/assets/images/icons/cloud/integrations.svg @@ -0,0 +1 @@ + diff --git a/dashboard/assets/images/icons/cloud/lighthouse-ai.svg b/dashboard/assets/images/icons/cloud/lighthouse-ai.svg new file mode 100644 index 0000000000..1c510ac1fc --- /dev/null +++ b/dashboard/assets/images/icons/cloud/lighthouse-ai.svg @@ -0,0 +1 @@ + diff --git a/dashboard/assets/images/icons/cloud/mutelist.svg b/dashboard/assets/images/icons/cloud/mutelist.svg new file mode 100644 index 0000000000..1d498fa9e3 --- /dev/null +++ b/dashboard/assets/images/icons/cloud/mutelist.svg @@ -0,0 +1 @@ + diff --git a/dashboard/assets/images/icons/cloud/organization.svg b/dashboard/assets/images/icons/cloud/organization.svg new file mode 100644 index 0000000000..1f3d1da9f5 --- /dev/null +++ b/dashboard/assets/images/icons/cloud/organization.svg @@ -0,0 +1 @@ + diff --git a/dashboard/assets/images/icons/cloud/overview.svg b/dashboard/assets/images/icons/cloud/overview.svg new file mode 100644 index 0000000000..809e63d945 --- /dev/null +++ b/dashboard/assets/images/icons/cloud/overview.svg @@ -0,0 +1,4 @@ + diff --git a/dashboard/assets/images/icons/cloud/prowler-lockup.svg b/dashboard/assets/images/icons/cloud/prowler-lockup.svg new file mode 100644 index 0000000000..9daee36463 --- /dev/null +++ b/dashboard/assets/images/icons/cloud/prowler-lockup.svg @@ -0,0 +1 @@ +LOCALDASHBOARD diff --git a/dashboard/assets/images/icons/cloud/prowler-mark.svg b/dashboard/assets/images/icons/cloud/prowler-mark.svg new file mode 100644 index 0000000000..eb30d44be8 --- /dev/null +++ b/dashboard/assets/images/icons/cloud/prowler-mark.svg @@ -0,0 +1 @@ + diff --git a/dashboard/pages/cloud.py b/dashboard/pages/cloud.py new file mode 100644 index 0000000000..3a6ce48b62 --- /dev/null +++ b/dashboard/pages/cloud.py @@ -0,0 +1,263 @@ +"""Prowler Cloud upsell (gated) informational pages. + +These routes live inside the Local Dashboard but do NOT reproduce any Prowler +Cloud functionality. Each renders the same reusable upgrade template with a +feature-specific name, icon, description, benefit bullets and UTM-tagged CTA. +All copy in ``CLOUD_FEATURES`` is normative — do not change wording, +capitalization or punctuation without Product approval. +""" + +import dash +from dash import html + +# Shared subtitle used across every gated page header. +CLOUD_SUBTITLE = "Discover more ways to protect and operate your cloud." + +# Base Prowler Cloud URL; the UTM content value identifies the feature. +CLOUD_CTA_BASE = "https://cloud.prowler.com/?utm_source=local-dashboard&utm_content=" + +# Path to the recolorable checkmark mask used for benefit bullets. +CHECK_ICON = "/assets/images/icons/cloud/check.svg" + + +# Normative feature definitions. ``icon`` points to a local mask asset so no +# external requests are needed and the glyph recolors per context. +CLOUD_FEATURES = [ + { + "slug": "lighthouse-ai", + "route": "/cloud/lighthouse-ai", + "nav_label": "Lighthouse AI", + "page_title": "Lighthouse AI", + "card_title": "Unlock Lighthouse AI", + "description": ( + "Work with an AI security analyst that understands your cloud " + "posture and helps turn risk into action." + ), + "benefits": [ + "Ask questions about your security posture in plain language", + "Investigate findings with context from your connected providers", + "Move from insight to remediation faster", + ], + "utm_content": "lighthouse-ai", + "icon": "/assets/images/icons/cloud/lighthouse-ai.svg", + }, + { + "slug": "attack-paths", + "route": "/cloud/attack-paths", + "nav_label": "Attack Paths", + "page_title": "Attack Paths", + "card_title": "Unlock Attack Paths", + "description": ( + "Visualize the paths an attacker could take through connected " + "resources before risk becomes compromise." + ), + "benefits": [ + "See exploitable relationships across your AWS environment", + "Focus remediation on the paths with the greatest impact", + "Explore each scan as a point-in-time security graph", + ], + "utm_content": "attack-paths", + "icon": "/assets/images/icons/cloud/attack-paths.svg", + }, + { + "slug": "findings", + "route": "/cloud/findings", + "nav_label": "Findings", + "page_title": "Findings", + "card_title": "Unlock Findings", + "description": ( + "Filter, investigate, and prioritize security findings across " + "providers and scans from one workspace." + ), + "benefits": [ + "Search and filter findings across all connected accounts", + "Track status, severity, ownership, and remediation context", + "Triage findings and share a consistent source of truth with your security team", + ], + "utm_content": "findings", + "icon": "/assets/images/icons/cloud/findings.svg", + }, + { + "slug": "alerts", + "route": "/cloud/alerts", + "nav_label": "Alerts", + "page_title": "Alerts", + "card_title": "Unlock Alerts", + "description": ( + "Create alert rules and stay informed when scan results reveal " + "the risks your team cares about." + ), + "benefits": [ + "Define alerts around the findings that matter most", + "Route security signals to the right responders", + "Reduce the time between detection and action", + ], + "utm_content": "alerts", + "icon": "/assets/images/icons/cloud/alerts.svg", + }, + { + "slug": "mutelist", + "route": "/cloud/mutelist", + "nav_label": "Mutelist", + "page_title": "Mutelist", + "card_title": "Unlock Mutelist", + "description": ( + "Quiet expected findings, document accepted risk, and keep your " + "team focused on actionable work." + ), + "benefits": [ + "Create reusable rules for known exceptions", + "Keep muted findings available for audit and review", + "Cut noise without losing security context", + ], + "utm_content": "mutelist", + "icon": "/assets/images/icons/cloud/mutelist.svg", + }, + { + "slug": "integrations", + "route": "/cloud/integrations", + "nav_label": "Integrations", + "page_title": "Integrations", + "card_title": "Unlock Integrations", + "description": ( + "Connect Prowler to your security workflow so findings and scan " + "data reach the tools your team already uses." + ), + "benefits": [ + "Connect ticketing, notification, and cloud security services", + "Automate the handoff from detection to response", + "Keep teams aligned without manual exports", + ], + "utm_content": "integrations", + "icon": "/assets/images/icons/cloud/integrations.svg", + }, + { + "slug": "organization", + "route": "/cloud/organization", + "nav_label": "Organization", + "page_title": "Organization", + "card_title": "Unlock Organization", + "description": ( + "Manage users, roles, and invitations while organizing cloud " + "security work across your team." + ), + "benefits": [ + "Invite teammates into a shared security workspace", + "Control access with role-based permissions", + "Coordinate security operations across accounts and teams", + ], + "utm_content": "organization", + "icon": "/assets/images/icons/cloud/organization.svg", + }, +] + +# Convenience lookup for the navigation builder in ``__main__``. +CLOUD_FEATURES_BY_SLUG = {feature["slug"]: feature for feature in CLOUD_FEATURES} + + +def _mask_style(icon_url): + """Return the inline style that renders a recolorable mask icon.""" + return { + "WebkitMaskImage": f"url({icon_url})", + "maskImage": f"url({icon_url})", + } + + +def _benefit_item(text): + return html.Li( + [ + html.Span( + className="pc-ico pc-benefit-check", + style=_mask_style(CHECK_ICON), + ), + html.Span(text), + ], + className="pc-benefit", + ) + + +def build_cloud_layout(feature): + """Build the reusable gated informational page for a single feature.""" + cta_url = f"{CLOUD_CTA_BASE}{feature['utm_content']}" + + return html.Div( + html.Div( + [ + # Page header: title + Prowler Cloud badge + shared subtitle. + html.Div( + [ + html.Div( + [ + html.H1( + feature["page_title"], + className="pc-page-title", + ), + html.Span("Prowler Cloud", className="pc-pill"), + ], + className="pc-page-title-row", + ), + html.P(CLOUD_SUBTITLE, className="pc-page-subtitle"), + ], + className="pc-page-header", + ), + # Centered upgrade card. + html.Div( + [ + html.Div(className="pc-card-glow"), + html.Div( + [ + html.Div( + html.Span( + className="pc-ico", + style=_mask_style(feature["icon"]), + ), + className="pc-feature-icon", + ), + html.Div( + "Available in Prowler Cloud", + className="pc-avail", + ), + html.H2( + feature["card_title"], + className="pc-card-title", + ), + html.P( + feature["description"], + className="pc-card-desc", + ), + html.Ul( + [ + _benefit_item(benefit) + for benefit in feature["benefits"] + ], + className="pc-benefits", + ), + html.A( + "Upgrade to Prowler Cloud", + href=cta_url, + target="_blank", + rel="noopener noreferrer", + className="pc-cta", + ), + ], + className="pc-card-body", + ), + ], + className="pc-card", + ), + ], + className="pc-page pc-font", + ), + ) + + +# Register one page per gated feature. A distinct module key keeps each entry +# unique in Dash's page registry while sharing the same template. +for _feature in CLOUD_FEATURES: + dash.register_page( + f"cloud_{_feature['slug'].replace('-', '_')}", + path=_feature["route"], + name=_feature["nav_label"], + title=f"Prowler Dashboard - {_feature['page_title']}", + layout=build_cloud_layout(_feature), + ) diff --git a/prowler/changelog.d/dashboard-local-redesign.changed.md b/prowler/changelog.d/dashboard-local-redesign.changed.md new file mode 100644 index 0000000000..59ba9ca7ea --- /dev/null +++ b/prowler/changelog.d/dashboard-local-redesign.changed.md @@ -0,0 +1 @@ +Redesign the local dashboard sidebar and informational pages