fix(dashboard): Add multiple dashboard fixes (#3714)

This commit is contained in:
Pedro Martín
2024-04-09 10:22:03 +02:00
committed by GitHub
parent 397cc26b2a
commit 082f3a8fe8
6 changed files with 438 additions and 49 deletions

View File

@@ -591,7 +591,7 @@ video {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
font-size: 0.875rem;
font-weight: 700;
font-weight: 400;
--tw-text-opacity: 1;
color: rgb(41 37 36 / var(--tw-text-opacity));
}

View File

@@ -6,6 +6,24 @@ fail_emoji = "❌"
info_emoji = ""
manual_emoji = "✋🏽"
# Main colors
fail_color = "#e67272"
pass_color = "#54d283"
info_color = "#2684FF"
manual_color = "#636c78"
# Muted colors
muted_fail_color = "#fca903"
muted_pass_color = "#03fccf"
muted_manual_color = "#b33696"
# Severity colors
critical_color = "#951649"
high_color = "#e11d48"
medium_color = "#ee6f15"
low_color = "#f9f5e6"
informational_color = "#3274d9"
# Folder output path
folder_path_overview = os.getcwd() + "/output"
folder_path_compliance = os.getcwd() + "/output/compliance"

View File

@@ -193,3 +193,97 @@ def create_compliance_dropdown(compliance: list) -> html.Div:
),
],
)
def create_severity_dropdown(severity: list) -> html.Div:
"""
Dropdown to select the severity.
Args:
severity (list): List of severity.
Returns:
html.Div: Dropdown to select the severity.
"""
return html.Div(
[
html.Label(
"Severity:", className="text-prowler-stone-900 font-bold text-sm"
),
dcc.Dropdown(
id="severity-filter",
options=[{"label": i, "value": i} for i in severity],
value=["All"],
clearable=False,
multi=True,
style={"color": "#000000"},
),
],
)
def create_service_dropdown(services: list) -> html.Div:
"""
Dropdown to select the service.
Args:
services (list): List of services.
Returns:
html.Div: Dropdown to select the service.
"""
return html.Div(
[
html.Label(
"Service:", className="text-prowler-stone-900 font-bold text-sm"
),
dcc.Dropdown(
id="service-filter",
options=[{"label": i, "value": i} for i in services],
value=["All"],
clearable=False,
multi=True,
style={"color": "#000000"},
),
],
)
def create_status_dropdown(status: list) -> html.Div:
"""
Dropdown to select the status.
Args:
status (list): List of status.
Returns:
html.Div: Dropdown to select the status.
"""
return html.Div(
[
html.Label("Status:", className="text-prowler-stone-900 font-bold text-sm"),
dcc.Dropdown(
id="status-filter",
options=[{"label": i, "value": i} for i in status],
value=["All"],
clearable=False,
multi=True,
style={"color": "#000000"},
),
],
)
def create_table_row_dropdown(table_rows: list) -> html.Div:
"""
Dropdown to select the number of rows in the table.
Args:
table_rows (list): List of number of rows.
Returns:
html.Div: Dropdown to select the number of rows in the table.
"""
return html.Div(
[
dcc.Dropdown(
id="table-rows",
options=[{"label": i, "value": i} for i in table_rows],
value=table_rows[0],
clearable=False,
style={"color": "#000000", "margin-right": "10px"},
),
],
)

View File

@@ -6,6 +6,10 @@ def create_layout_overview(
date_dropdown: html.Div,
region_dropdown: html.Div,
download_button: html.Button,
severity_dropdown: html.Div,
service_dropdown: html.Div,
table_row_dropdown: html.Div,
status_dropdown: html.Div,
) -> html.Div:
"""
Create the layout of the dashboard.
@@ -39,15 +43,23 @@ def create_layout_overview(
),
html.Div(
[
html.Div(className="flex", id="aws_card"),
html.Div(className="flex", id="azure_card"),
html.Div(className="flex", id="gcp_card"),
html.Div(className="flex", id="k8s_card"),
html.Div([severity_dropdown], className=""),
html.Div([service_dropdown], className=""),
html.Div([status_dropdown], className=""),
],
className="grid gap-x-4 gap-y-4 sm:grid-cols-2 lg:grid-cols-3 lg:gap-y-0",
),
html.Div(
[
html.Div(className="flex", id="aws_card", n_clicks=0),
html.Div(className="flex", id="azure_card", n_clicks=0),
html.Div(className="flex", id="gcp_card", n_clicks=0),
html.Div(className="flex", id="k8s_card", n_clicks=0),
],
className="grid gap-x-4 gap-y-4 sm:grid-cols-2 lg:grid-cols-4 lg:gap-y-0",
),
html.H4(
"Count of Failed Findings by severity",
"Count of Findings by severity",
className="text-prowler-stone-900 text-lg font-bold",
),
html.Div(
@@ -70,10 +82,23 @@ def create_layout_overview(
html.Div(
[
html.H4(
"Top 25 Failed Findings by Severity",
"Top Findings by Severity",
className="text-prowler-stone-900 text-lg font-bold",
),
download_button,
html.Div(
[
(
html.Label(
"Table Rows:",
className="text-prowler-stone-900 font-bold text-sm",
style={"margin-right": "10px"},
)
),
table_row_dropdown,
download_button,
],
className="flex justify-between items-center",
),
dcc.Download(id="download-data"),
],
className="flex justify-between items-center",

View File

@@ -14,7 +14,13 @@ from dash import callback, dcc, html
from dash.dependencies import Input, Output
# Config import
from dashboard.config import folder_path_compliance
from dashboard.config import (
fail_color,
folder_path_compliance,
info_color,
manual_color,
pass_color,
)
from dashboard.lib.dropdowns import (
create_account_dropdown_compliance,
create_compliance_dropdown,
@@ -505,7 +511,7 @@ def get_bar_graph(df, column_name):
x="counts",
y=colums,
color="STATUS",
color_discrete_map={"FAIL": "#e67272"},
color_discrete_map={"FAIL": fail_color},
orientation="h",
)
@@ -530,11 +536,11 @@ def get_bar_graph(df, column_name):
def get_pie(df):
# Define custom colors
color_mapping = {
"FAIL": "#e67272",
"PASS": "#54d283",
"INFO": "#2684FF",
"FAIL": fail_color,
"PASS": pass_color,
"INFO": info_color,
"WARN": "#260000",
"MANUAL": "#636c78",
"MANUAL": manual_color,
}
# Use the color_discrete_map parameter to map categories to custom colors

View File

@@ -16,12 +16,30 @@ from dash import callback, ctx, dcc, html
from dash.dependencies import Input, Output
# Config import
from dashboard.config import folder_path_overview
from dashboard.config import (
critical_color,
fail_color,
folder_path_overview,
high_color,
info_color,
informational_color,
low_color,
manual_color,
medium_color,
muted_fail_color,
muted_manual_color,
muted_pass_color,
pass_color,
)
from dashboard.lib.cards import create_provider_card
from dashboard.lib.dropdowns import (
create_account_dropdown,
create_date_dropdown,
create_region_dropdown,
create_service_dropdown,
create_severity_dropdown,
create_status_dropdown,
create_table_row_dropdown,
)
from dashboard.lib.layouts import create_layout_overview
@@ -221,6 +239,33 @@ else:
regions = options
region_dropdown = create_region_dropdown(regions)
# Severity Dropdown
severity = ["All"] + list(data["SEVERITY"].unique())
severity = [
x for x in severity if str(x) != "nan" and x.__class__.__name__ == "str"
]
severity_dropdown = create_severity_dropdown(severity)
# Service Dropdown
services = []
for service in data["SERVICE_NAME"].unique():
if "aws" in list(data[data["SERVICE_NAME"] == service]["PROVIDER"]):
services.append(service + " - AWS")
if "kubernetes" in list(data[data["SERVICE_NAME"] == service]["PROVIDER"]):
services.append(service + " - K8S")
if "azure" in list(data[data["SERVICE_NAME"] == service]["PROVIDER"]):
services.append(service + " - AZURE")
if "gcp" in list(data[data["SERVICE_NAME"] == service]["PROVIDER"]):
services.append(service + " - GCP")
services = ["All"] + services
services = [
x for x in services if str(x) != "nan" and x.__class__.__name__ == "str"
]
service_dropdown = create_service_dropdown(services)
# Create the download button
download_button = html.Button(
"Download this table as CSV",
@@ -229,12 +274,29 @@ else:
className="border-solid border-2 border-prowler-stone-900/10 hover:border-solid hover:border-2 hover:border-prowler-stone-900/10 text-prowler-stone-900 inline-block px-4 py-2 text-xs font-bold uppercase transition-all rounded-lg text-gray-900 hover:bg-prowler-stone-900/10 flex justify-end w-fit",
)
# Create the table row dropdown
table_row_values = [-1]
table_row_dropdown = create_table_row_dropdown(table_row_values)
# Create the status dropdown
status = ["All"] + list(data["STATUS"].unique())
status = [x for x in status if str(x) != "nan" and x.__class__.__name__ == "str"]
status_dropdown = create_status_dropdown(status)
# Initializing the Dash App
dash.register_page(__name__, path="/")
# Create the layout
layout = create_layout_overview(
account_dropdown, date_dropdown, region_dropdown, download_button
account_dropdown,
date_dropdown,
region_dropdown,
download_button,
severity_dropdown,
service_dropdown,
table_row_dropdown,
status_dropdown,
)
@@ -257,19 +319,80 @@ else:
Output("k8s_card", "children"),
Output("subscribe_card", "children"),
Output("info-file-over", "title"),
Output("severity-filter", "value"),
Output("severity-filter", "options"),
Output("service-filter", "value"),
Output("service-filter", "options"),
Output("table-rows", "value"),
Output("table-rows", "options"),
Output("status-filter", "value"),
Output("status-filter", "options"),
Output("aws_card", "n_clicks"),
Output("azure_card", "n_clicks"),
Output("gcp_card", "n_clicks"),
Output("k8s_card", "n_clicks"),
],
Input("cloud-account-filter", "value"),
Input("region-filter", "value"),
Input("report-date-filter", "value"),
Input("download_link", "n_clicks"),
Input("severity-filter", "value"),
Input("service-filter", "value"),
Input("table-rows", "value"),
Input("status-filter", "value"),
Input("aws_card", "n_clicks"),
Input("azure_card", "n_clicks"),
Input("gcp_card", "n_clicks"),
Input("k8s_card", "n_clicks"),
)
def filter_data(
cloud_account_values, region_account_values, assessment_value, n_clicks
cloud_account_values,
region_account_values,
assessment_value,
n_clicks,
severity_values,
service_values,
table_row_values,
status_values,
aws_clicks,
azure_clicks,
gcp_clicks,
k8s_clicks,
):
# Use n_clicks for vulture
n_clicks = n_clicks
# Filter the data
filtered_data = data.copy()
if aws_clicks > 0:
filtered_data = data.copy()
if aws_clicks % 2 != 0 and "aws" in list(data["PROVIDER"]):
filtered_data = filtered_data[filtered_data["PROVIDER"] == "aws"]
azure_clicks = 0
gcp_clicks = 0
k8s_clicks = 0
if azure_clicks > 0:
filtered_data = data.copy()
if azure_clicks % 2 != 0 and "azure" in list(data["PROVIDER"]):
filtered_data = filtered_data[filtered_data["PROVIDER"] == "azure"]
aws_clicks = 0
gcp_clicks = 0
k8s_clicks = 0
if gcp_clicks > 0:
filtered_data = data.copy()
if gcp_clicks % 2 != 0 and "gcp" in list(data["PROVIDER"]):
filtered_data = filtered_data[filtered_data["PROVIDER"] == "gcp"]
aws_clicks = 0
azure_clicks = 0
k8s_clicks = 0
if k8s_clicks > 0:
filtered_data = data.copy()
if k8s_clicks % 2 != 0 and "kubernetes" in list(data["PROVIDER"]):
filtered_data = filtered_data[filtered_data["PROVIDER"] == "kubernetes"]
aws_clicks = 0
azure_clicks = 0
gcp_clicks = 0
# For all the data, we will add to the status column the value 'MUTED (FAIL)' and 'MUTED (PASS)' depending on the value of the column 'STATUS' and 'MUTED'
if "MUTED" in filtered_data.columns:
filtered_data["STATUS"] = filtered_data.apply(
@@ -422,7 +545,6 @@ def filter_data(
elif "ACCOUNT_NAME" in filtered_data.columns:
filtered_data = filtered_data[filtered_data["ACCOUNT_NAME"].isin(values_choice)]
copy_data = filtered_data.copy()
# Filter REGION
# Check if filtered data contains an aws account
@@ -446,7 +568,7 @@ def filter_data(
filtered_data["REGION"].isin(updated_region_account_values)
]
region_filter_options = ["All"] + list(copy_data["REGION"].unique())
region_filter_options = ["All"] + list(filtered_data["REGION"].unique())
# clean the region_filter_options from null values
region_filter_options = [
x
@@ -463,10 +585,84 @@ def filter_data(
region_filter_options = options
# Select failed findings
fails_findings_default = filtered_data[filtered_data["STATUS"] == "FAIL"]
fails_findings_muted = filtered_data[filtered_data["STATUS"] == "MUTED (FAIL)"]
fails_findings = pd.concat([fails_findings_default, fails_findings_muted])
# Filter Severity
if severity_values == ["All"]:
updated_severity_values = filtered_data["SEVERITY"].unique()
elif "All" in severity_values and len(severity_values) > 1:
# Remove 'All' from the list
severity_values.remove("All")
updated_severity_values = severity_values
elif len(severity_values) == 0:
updated_severity_values = filtered_data["SEVERITY"].unique()
severity_values = ["All"]
else:
updated_severity_values = severity_values
filtered_data = filtered_data[
filtered_data["SEVERITY"].isin(updated_severity_values)
]
severity_filter_options = ["All"] + list(filtered_data["SEVERITY"].unique())
service_filter_options = ["All"]
all_items = filtered_data["SERVICE_NAME"].unique()
for item in all_items:
if item not in service_filter_options and item.__class__.__name__ == "str":
if "aws" in list(
filtered_data[filtered_data["SERVICE_NAME"] == item]["PROVIDER"]
):
service_filter_options.append(item + " - AWS")
if "kubernetes" in list(
filtered_data[filtered_data["SERVICE_NAME"] == item]["PROVIDER"]
):
service_filter_options.append(item + " - K8S")
if "azure" in list(
filtered_data[filtered_data["SERVICE_NAME"] == item]["PROVIDER"]
):
service_filter_options.append(item + " - AZURE")
if "gcp" in list(
filtered_data[filtered_data["SERVICE_NAME"] == item]["PROVIDER"]
):
service_filter_options.append(item + " - GCP")
# Filter Service
if service_values == ["All"]:
updated_service_values = filtered_data["SERVICE_NAME"].unique()
elif "All" in service_values and len(service_values) > 1:
# Remove 'All' from the list
updated_service_values = []
service_values.remove("All")
for item in service_values:
updated_service_values.append(item.split(" - ")[0])
elif len(service_values) == 0:
updated_service_values = filtered_data["SERVICE_NAME"].unique()
service_values = ["All"]
else:
updated_service_values = []
for item in service_values:
updated_service_values.append(item.split(" - ")[0])
filtered_data = filtered_data[
filtered_data["SERVICE_NAME"].isin(updated_service_values)
]
# Filter Status
if status_values == ["All"]:
updated_status_values = filtered_data["STATUS"].unique()
elif "All" in status_values and len(status_values) > 1:
# Remove 'All' from the list
status_values.remove("All")
updated_status_values = status_values
elif len(status_values) == 0:
updated_status_values = filtered_data["STATUS"].unique()
status_values = ["All"]
else:
updated_status_values = status_values
filtered_data = filtered_data[filtered_data["STATUS"].isin(updated_status_values)]
status_filter_options = ["All"] + list(filtered_data["STATUS"].unique())
if len(filtered_data_sp) == 0:
fig = px.pie()
@@ -521,13 +717,13 @@ def filter_data(
result_df["Status_count"].fillna(0, inplace=True)
color_mapping = {
"FAIL": "#e67272",
"PASS": "#54d283",
"INFO": "#2684FF",
"MANUAL": "#636c78",
"MUTED (FAIL)": "#fca903",
"MUTED (PASS)": "#03fccf",
"MUTED (MANUAL)": "#b33696",
"FAIL": fail_color,
"PASS": pass_color,
"INFO": info_color,
"MANUAL": manual_color,
"MUTED (FAIL)": muted_fail_color,
"MUTED (PASS)": muted_pass_color,
"MUTED (MANUAL)": muted_manual_color,
}
# Create a single line plot for both 'FAIL' and 'PASS' statuses
@@ -576,23 +772,23 @@ def filter_data(
df1 = filtered_data[filtered_data["STATUS"] == "FAIL"]
color_mapping_pass_fail = {
"FAIL": "#e67272",
"PASS": "#54d283",
"INFO": "#2684FF",
"MANUAL": "#636c78",
"WARNING": "#fca903",
"MUTED (FAIL)": "#fca903",
"MUTED (PASS)": "#03fccf",
"FAIL": fail_color,
"PASS": pass_color,
"INFO": info_color,
"MANUAL": manual_color,
"WARNING": muted_fail_color,
"MUTED (FAIL)": muted_fail_color,
"MUTED (PASS)": muted_pass_color,
"MUTED (MANUAL)": "#b33696",
"MUTED (WARNING)": "#c7a45d",
}
# Define custom colors
color_mapping = {
"critical": "#951649",
"high": "#e11d48",
"medium": "#ee6f15",
"low": "#f9f5e6",
"informational": "#3274d9",
"critical": critical_color,
"high": high_color,
"medium": medium_color,
"low": low_color,
"informational": informational_color,
}
# Use the color_discrete_map parameter to map categories to custom colors
@@ -661,12 +857,13 @@ def filter_data(
"low": 1,
"informational": 0,
}
fails_findings["SEVERITY"] = fails_findings["SEVERITY"].map(severity_dict)
fails_findings = fails_findings.sort_values(by=["SEVERITY"], ascending=False)
fails_findings["SEVERITY"] = fails_findings["SEVERITY"].replace(
filtered_data["SEVERITY"] = filtered_data["SEVERITY"].map(severity_dict)
filtered_data = filtered_data.sort_values(by=["SEVERITY"], ascending=False)
filtered_data["SEVERITY"] = filtered_data["SEVERITY"].replace(
{4: "critical", 3: "high", 2: "medium", 1: "low", 0: "informational"}
)
table_data = fails_findings.copy()
table_data = filtered_data.copy()
if "ACCOUNT_NAME" in table_data.columns:
for subscription in table_data["ACCOUNT_NAME"].unique():
@@ -704,15 +901,38 @@ def filter_data(
}
)
if len(table_data) > 25:
table_row_options = []
# Take the values from the table_row_values
if table_row_values == -1:
if len(table_data) >= 25:
table_row_values = 25
else:
table_row_values = "Full"
if len(table_data) < 25:
table_row_values = "Full"
if len(table_data) >= 25:
table_row_options.append(25)
if len(table_data) >= 50:
table_row_options.append(50)
if len(table_data) >= 75:
table_row_options.append(75)
if len(table_data) >= 100:
table_row_options.append(100)
table_row_options.append("Full")
if table_row_values == "Full":
table = dbc.Table.from_dataframe(
table_data[:25],
table_data,
striped=True,
bordered=False,
hover=True,
className="table-overview",
)
else:
table_data = table_data[:table_row_values]
table = dbc.Table.from_dataframe(
table_data,
striped=True,
@@ -788,7 +1008,9 @@ def filter_data(
)
]
if ctx.triggered_id == "download_link":
csv_data = dcc.send_data_frame(table_data[:25].to_csv, "mydf.csv")
csv_data = dcc.send_data_frame(
table_data.to_csv, "prowler-dashboard-export.csv"
)
return (
status_graph,
two_pie_chart,
@@ -806,6 +1028,18 @@ def filter_data(
k8s_card,
subscribe_card,
list_files,
severity_values,
severity_filter_options,
service_values,
service_filter_options,
table_row_values,
table_row_options,
status_values,
status_filter_options,
aws_clicks,
azure_clicks,
gcp_clicks,
k8s_clicks,
)
else:
return (
@@ -825,4 +1059,16 @@ def filter_data(
k8s_card,
subscribe_card,
list_files,
severity_values,
severity_filter_options,
service_values,
service_filter_options,
table_row_values,
table_row_options,
status_values,
status_filter_options,
aws_clicks,
azure_clicks,
gcp_clicks,
k8s_clicks,
)