feat(dashboard): redesign local dashboard sidebar and pages (#11972)
@@ -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)
|
||||
|
||||
@@ -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 |
@@ -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
|
||||