feat(dashboard): redesign local dashboard sidebar and pages (#11972)

This commit is contained in:
Daniel Barranquero
2026-07-14 14:40:21 +02:00
committed by GitHub
parent bd72ec91ea
commit 9822fd97d7
18 changed files with 826 additions and 114 deletions
+153 -114
View File
@@ -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://<your-bucket>/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)
+389
View File
@@ -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;
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M22 8c0-2.3-.8-4.3-2-6"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/><path d="M4 2C2.8 3.7 2 5.7 2 8"/></svg>

After

Width:  |  Height:  |  Size: 402 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>

After

Width:  |  Height:  |  Size: 284 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M4.5 12.75l6 6 9-13.5"/></svg>

After

Width:  |  Height:  |  Size: 192 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFF" aria-hidden="true" class="h-5 w-5" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M9 1.5H5.625c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5zm6.61 10.936a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 14.47a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25z" clip-rule="evenodd"/>
<path d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963z"/>
</svg>

After

Width:  |  Height:  |  Size: 650 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>

After

Width:  |  Height:  |  Size: 303 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 363 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"/></svg>

After

Width:  |  Height:  |  Size: 370 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M15.39 4.39a1 1 0 0 0 1.68-.474 2.5 2.5 0 1 1 3.014 3.015 1 1 0 0 0-.474 1.68l1.683 1.682a2.414 2.414 0 0 1 0 3.414L19.61 15.39a1 1 0 0 1-1.68-.474 2.5 2.5 0 1 0-3.014 3.015 1 1 0 0 1 .474 1.68l-1.683 1.682a2.414 2.414 0 0 1-3.414 0L8.61 19.61a1 1 0 0 0-1.68.474 2.5 2.5 0 1 1-3.014-3.015 1 1 0 0 0 .474-1.68l-1.683-1.682a2.414 2.414 0 0 1 0-3.414L4.39 8.61a1 1 0 0 1 1.68.474 2.5 2.5 0 1 0 3.014-3.015 1 1 0 0 1-.474-1.68l1.683-1.682a2.414 2.414 0 0 1 3.414 0z"/></svg>

After

Width:  |  Height:  |  Size: 630 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/></svg>

After

Width:  |  Height:  |  Size: 500 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"/><line x1="22" x2="16" y1="9" y2="15"/><line x1="16" x2="22" y1="9" y2="15"/></svg>

After

Width:  |  Height:  |  Size: 413 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg>

After

Width:  |  Height:  |  Size: 316 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFF" aria-hidden="true" class="h-5 w-5" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M2.25 13.5a8.25 8.25 0 0 1 8.25-8.25.75.75 0 0 1 .75.75v6.75H18a.75.75 0 0 1 .75.75 8.25 8.25 0 0 1-16.5 0z" clip-rule="evenodd"/>
<path fill-rule="evenodd" d="M12.75 3a.75.75 0 0 1 .75-.75 8.25 8.25 0 0 1 8.25 8.25.75.75 0 0 1-.75.75h-7.5a.75.75 0 0 1-.75-.75V3z" clip-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 435 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="88.14" viewBox="0 0 200 88.14"><defs><linearGradient id="lh" x1="0" y1="0" x2="1" y2="0.14"><stop offset="0" stop-color="#2EE59B"></stop><stop offset="1" stop-color="#62DFF0"></stop></linearGradient></defs><g transform="scale(0.1621179084)"><path d="M1169.38,132.04c20.76-12.21,34.44-34.9,34.44-59.79,0-38.18-31.06-69.25-69.25-69.25l-216.9.23v145.17h-64.8V3h-79.95s-47.14,95.97-47.14,95.97V3h-52.09s-47.14,95.97-47.14,95.97V3h-53.48v69.6C560.37,30.64,521.34,0,475.28,0c-42.63,0-79.24,26.25-94.54,63.43-4.35-34.03-33.48-60.43-68.67-60.43h-100.01v47.43C202.9,22.91,176.91,3,146.35,3H0s46.34,46.33,46.34,46.33v151.64h53.47v-76.68l17.21,17.21h29.33c30.56,0,56.54-19.91,65.71-47.43v106.91h53.48v-81.51l76.01,81.51h69.62l-64.29-68.94c11.14-6.56,20.22-16.15,26.26-27.46,1.27,55.26,46.58,99.82,102.14,99.82,46.06,0,85.09-30.64,97.81-72.6v69.18h60.88l38.34-78.06v78.06h60.88l66.2-134.78v135.69h95.41l22.86-22.86v22.86h95.05l21.84-21.84v20.93h53.48v-81.5l76.01,81.5h69.62l-64.29-68.94ZM146.35,88.02h-46.54v-31.54h46.54c8.7,0,15.77,7.07,15.77,15.77s-7.07,15.77-15.77,15.77ZM312.07,88.02l-46.54-.18v-31.36h46.54c8.7,0,15.77,7.07,15.77,15.77s-7.08,15.77-15.77,15.77ZM475.28,150.92c-26.86,0-48.72-21.86-48.72-48.72s21.86-48.72,48.72-48.72,48.72,21.86,48.72,48.72-21.86,48.72-48.72,48.72ZM1034.56,148.41h-63.41v-20.35h42.91v-50.88h-42.91v-20.46h63.41v91.69ZM1134.57,88.02l-46.54-.18v-31.36h46.54c8.7,0,15.77,7.07,15.77,15.77s-7.07,15.77-15.77,15.77Z" fill="#ffffff"></path></g><text x="200" y="59.14" text-anchor="end" font-family="ProwlerInter, Inter, sans-serif" font-weight="600" font-size="19" letter-spacing="0.7" fill="url(#lh)">LOCAL</text><text x="200" y="84.14" text-anchor="end" font-family="ProwlerInter, Inter, sans-serif" font-weight="600" font-size="19" letter-spacing="0.7" fill="url(#lh)">DASHBOARD</text></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#8CE046" fill-rule="evenodd" clip-rule="evenodd" d="M7 4h9.5a8 8 0 0 1 0 16H12.5v8H7V4Zm5.5 5v6H16a3 3 0 1 0 0-6h-3.5Z"/></svg>

After

Width:  |  Height:  |  Size: 200 B

+263
View File
@@ -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),
)
@@ -0,0 +1 @@
Redesign the local dashboard sidebar and informational pages