From 16cd0e4661478053e7e5b8907d5308bf6c01c827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Wed, 21 May 2025 12:46:47 +0200 Subject: [PATCH] feat(prowler_threatscore): add a level for accordion in dashboard (#7739) Co-authored-by: Sergio Garcia --- dashboard/common_methods.py | 350 ++++++++++++++++++ .../compliance/prowler_threatscore_aws.py | 10 +- .../compliance/prowler_threatscore_azure.py | 10 +- .../compliance/prowler_threatscore_gcp.py | 10 +- .../compliance/prowler_threatscore_m365.py | 10 +- prowler/CHANGELOG.md | 7 +- 6 files changed, 382 insertions(+), 15 deletions(-) diff --git a/dashboard/common_methods.py b/dashboard/common_methods.py index 338b6e6473..a2f9ffe89b 100644 --- a/dashboard/common_methods.py +++ b/dashboard/common_methods.py @@ -2569,6 +2569,356 @@ def get_section_containers_3_levels(data, section_1, section_2, section_3): return html.Div(section_containers, className="compliance-data-layout") +def get_section_containers_threatscore(data, section_1, section_2, section_3): + data["STATUS"] = data["STATUS"].apply(map_status_to_icon) + findings_counts_marco = ( + data.groupby([section_1, "STATUS"]).size().unstack(fill_value=0) + ) + section_containers = [] + data[section_1] = data[section_1].astype(str) + data[section_2] = data[section_2].astype(str) + data[section_3] = data[section_3].astype(str) + + data.sort_values( + by=section_3, + key=lambda x: x.map(extract_numeric_values), + ascending=True, + inplace=True, + ) + + for marco in data[section_1].unique(): + success_marco = findings_counts_marco.loc[marco].get(pass_emoji, 0) + failed_marco = findings_counts_marco.loc[marco].get(fail_emoji, 0) + + fig_name = go.Figure( + [ + go.Bar( + name="Failed", + x=[failed_marco], + y=[""], + orientation="h", + marker=dict(color="#e77676"), + width=[0.8], + ), + go.Bar( + name="Success", + x=[success_marco], + y=[""], + orientation="h", + marker=dict(color="#45cc6e"), + width=[0.8], + ), + ] + ) + fig_name.update_layout( + barmode="stack", + margin=dict(l=10, r=10, t=10, b=10), + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + showlegend=False, + width=350, + height=30, + xaxis=dict(showticklabels=False, showgrid=False, zeroline=False), + yaxis=dict(showticklabels=False, showgrid=False, zeroline=False), + annotations=[ + dict( + x=success_marco + failed_marco, + y=0, + xref="x", + yref="y", + text=str(success_marco), + showarrow=False, + font=dict(color="#45cc6e", size=14), + xanchor="left", + yanchor="middle", + ), + dict( + x=0, + y=0, + xref="x", + yref="y", + text=str(failed_marco), + showarrow=False, + font=dict(color="#e77676", size=14), + xanchor="right", + yanchor="middle", + ), + ], + ) + fig_name.add_annotation( + x=failed_marco, + y=0.3, + text="|", + showarrow=False, + font=dict(size=20), + xanchor="center", + yanchor="middle", + ) + + graph_div = html.Div( + dcc.Graph( + figure=fig_name, config={"staticPlot": True}, className="info-bar" + ), + className="graph-section", + ) + direct_internal_items = [] + + for categoria in data[data[section_1] == marco][section_2].unique(): + specific_data = data[ + (data[section_1] == marco) & (data[section_2] == categoria) + ] + findings_counts_categoria = ( + specific_data.groupby([section_2, "STATUS"]) + .size() + .unstack(fill_value=0) + ) + success_categoria = findings_counts_categoria.loc[categoria].get( + pass_emoji, 0 + ) + failed_categoria = findings_counts_categoria.loc[categoria].get( + fail_emoji, 0 + ) + + fig_section = go.Figure( + [ + go.Bar( + name="Failed", + x=[failed_categoria], + y=[""], + orientation="h", + marker=dict(color="#e77676"), + width=[0.8], + ), + go.Bar( + name="Success", + x=[success_categoria], + y=[""], + orientation="h", + marker=dict(color="#45cc6e"), + width=[0.8], + ), + ] + ) + fig_section.update_layout( + barmode="stack", + margin=dict(l=10, r=10, t=10, b=10), + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + showlegend=False, + width=350, + height=30, + xaxis=dict(showticklabels=False, showgrid=False, zeroline=False), + yaxis=dict(showticklabels=False, showgrid=False, zeroline=False), + annotations=[ + dict( + x=success_categoria + failed_categoria, + y=0, + xref="x", + yref="y", + text=str(success_categoria), + showarrow=False, + font=dict(color="#45cc6e", size=14), + xanchor="left", + yanchor="middle", + ), + dict( + x=0, + y=0, + xref="x", + yref="y", + text=str(failed_categoria), + showarrow=False, + font=dict(color="#e77676", size=14), + xanchor="right", + yanchor="middle", + ), + ], + ) + fig_section.add_annotation( + x=failed_categoria, + y=0.3, + text="|", + showarrow=False, + font=dict(size=20), + xanchor="center", + yanchor="middle", + ) + + graph_div_section = html.Div( + dcc.Graph( + figure=fig_section, + config={"staticPlot": True}, + className="info-bar-child", + ), + className="graph-section-req", + ) + direct_internal_items_idgrupocontrol = [] + + for idgrupocontrol in specific_data[section_3].unique(): + specific_data2 = specific_data[ + specific_data[section_3] == idgrupocontrol + ] + findings_counts_idgrupocontrol = ( + specific_data2.groupby([section_3, "STATUS"]) + .size() + .unstack(fill_value=0) + ) + success_idgrupocontrol = findings_counts_idgrupocontrol.loc[ + idgrupocontrol + ].get(pass_emoji, 0) + failed_idgrupocontrol = findings_counts_idgrupocontrol.loc[ + idgrupocontrol + ].get(fail_emoji, 0) + + fig_idgrupocontrol = go.Figure( + [ + go.Bar( + name="Failed", + x=[failed_idgrupocontrol], + y=[""], + orientation="h", + marker=dict(color="#e77676"), + width=[0.8], + ), + go.Bar( + name="Success", + x=[success_idgrupocontrol], + y=[""], + orientation="h", + marker=dict(color="#45cc6e"), + width=[0.8], + ), + ] + ) + fig_idgrupocontrol.update_layout( + barmode="stack", + margin=dict(l=10, r=10, t=10, b=10), + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + showlegend=False, + width=350, + height=30, + xaxis=dict(showticklabels=False, showgrid=False, zeroline=False), + yaxis=dict(showticklabels=False, showgrid=False, zeroline=False), + annotations=[ + dict( + x=success_idgrupocontrol + failed_idgrupocontrol, + y=0, + xref="x", + yref="y", + text=str(success_idgrupocontrol), + showarrow=False, + font=dict(color="#45cc6e", size=14), + xanchor="left", + yanchor="middle", + ), + dict( + x=0, + y=0, + xref="x", + yref="y", + text=str(failed_idgrupocontrol), + showarrow=False, + font=dict(color="#e77676", size=14), + xanchor="right", + yanchor="middle", + ), + ], + ) + fig_idgrupocontrol.add_annotation( + x=failed_idgrupocontrol, + y=0.3, + text="|", + showarrow=False, + font=dict(size=20), + xanchor="center", + yanchor="middle", + ) + + graph_div_idgrupocontrol = html.Div( + dcc.Graph( + figure=fig_idgrupocontrol, + config={"staticPlot": True}, + className="info-bar-child", + ), + className="graph-section-req", + ) + + data_table = dash_table.DataTable( + data=specific_data2.to_dict("records"), + columns=[ + {"name": i, "id": i} + for i in [ + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ], + style_table={"overflowX": "auto"}, + style_as_list_view=True, + style_cell={"textAlign": "left", "padding": "5px"}, + ) + + title_internal = f"{idgrupocontrol} - {specific_data2['REQUIREMENTS_DESCRIPTION'].iloc[0]}" + + # Cut the title if it's too long + title_internal = ( + title_internal[:130] + " ..." + if len(title_internal) > 130 + else title_internal + ) + + internal_accordion_item_2 = dbc.AccordionItem( + title=title_internal, + children=[ + graph_div_idgrupocontrol, + html.Div([data_table], className="inner-accordion-content"), + ], + ) + direct_internal_items_idgrupocontrol.append( + html.Div( + [ + graph_div_idgrupocontrol, + dbc.Accordion( + [internal_accordion_item_2], + start_collapsed=True, + flush=True, + ), + ], + className="accordion-inner--child", + ) + ) + + internal_accordion_item = dbc.AccordionItem( + title=categoria, + children=direct_internal_items_idgrupocontrol, + ) + internal_section_container = html.Div( + [ + graph_div_section, + dbc.Accordion( + [internal_accordion_item], start_collapsed=True, flush=True + ), + ], + className="accordion-inner--child", + ) + direct_internal_items.append(internal_section_container) + + accordion_item = dbc.AccordionItem(title=marco, children=direct_internal_items) + section_container = html.Div( + [ + graph_div, + dbc.Accordion([accordion_item], start_collapsed=True, flush=True), + ], + className="accordion-inner", + ) + section_containers.append(section_container) + + return html.Div(section_containers, className="compliance-data-layout") + + # This function extracts and compares up to two numeric values, ensuring correct sorting for version-like strings. def extract_numeric_values(value): numbers = re.findall(r"\d+", str(value)) diff --git a/dashboard/compliance/prowler_threatscore_aws.py b/dashboard/compliance/prowler_threatscore_aws.py index 94558f33ad..d86a13fd01 100644 --- a/dashboard/compliance/prowler_threatscore_aws.py +++ b/dashboard/compliance/prowler_threatscore_aws.py @@ -1,6 +1,6 @@ import warnings -from dashboard.common_methods import get_section_containers_cis +from dashboard.common_methods import get_section_containers_threatscore warnings.filterwarnings("ignore") @@ -11,6 +11,7 @@ def get_table(data): "REQUIREMENTS_ID", "REQUIREMENTS_DESCRIPTION", "REQUIREMENTS_ATTRIBUTES_SECTION", + "REQUIREMENTS_ATTRIBUTES_SUBSECTION", "CHECKID", "STATUS", "REGION", @@ -19,6 +20,9 @@ def get_table(data): ] ].copy() - return get_section_containers_cis( - aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION" + return get_section_containers_threatscore( + aux, + "REQUIREMENTS_ATTRIBUTES_SECTION", + "REQUIREMENTS_ATTRIBUTES_SUBSECTION", + "REQUIREMENTS_ID", ) diff --git a/dashboard/compliance/prowler_threatscore_azure.py b/dashboard/compliance/prowler_threatscore_azure.py index 94558f33ad..d86a13fd01 100644 --- a/dashboard/compliance/prowler_threatscore_azure.py +++ b/dashboard/compliance/prowler_threatscore_azure.py @@ -1,6 +1,6 @@ import warnings -from dashboard.common_methods import get_section_containers_cis +from dashboard.common_methods import get_section_containers_threatscore warnings.filterwarnings("ignore") @@ -11,6 +11,7 @@ def get_table(data): "REQUIREMENTS_ID", "REQUIREMENTS_DESCRIPTION", "REQUIREMENTS_ATTRIBUTES_SECTION", + "REQUIREMENTS_ATTRIBUTES_SUBSECTION", "CHECKID", "STATUS", "REGION", @@ -19,6 +20,9 @@ def get_table(data): ] ].copy() - return get_section_containers_cis( - aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION" + return get_section_containers_threatscore( + aux, + "REQUIREMENTS_ATTRIBUTES_SECTION", + "REQUIREMENTS_ATTRIBUTES_SUBSECTION", + "REQUIREMENTS_ID", ) diff --git a/dashboard/compliance/prowler_threatscore_gcp.py b/dashboard/compliance/prowler_threatscore_gcp.py index 94558f33ad..d86a13fd01 100644 --- a/dashboard/compliance/prowler_threatscore_gcp.py +++ b/dashboard/compliance/prowler_threatscore_gcp.py @@ -1,6 +1,6 @@ import warnings -from dashboard.common_methods import get_section_containers_cis +from dashboard.common_methods import get_section_containers_threatscore warnings.filterwarnings("ignore") @@ -11,6 +11,7 @@ def get_table(data): "REQUIREMENTS_ID", "REQUIREMENTS_DESCRIPTION", "REQUIREMENTS_ATTRIBUTES_SECTION", + "REQUIREMENTS_ATTRIBUTES_SUBSECTION", "CHECKID", "STATUS", "REGION", @@ -19,6 +20,9 @@ def get_table(data): ] ].copy() - return get_section_containers_cis( - aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION" + return get_section_containers_threatscore( + aux, + "REQUIREMENTS_ATTRIBUTES_SECTION", + "REQUIREMENTS_ATTRIBUTES_SUBSECTION", + "REQUIREMENTS_ID", ) diff --git a/dashboard/compliance/prowler_threatscore_m365.py b/dashboard/compliance/prowler_threatscore_m365.py index 94558f33ad..d86a13fd01 100644 --- a/dashboard/compliance/prowler_threatscore_m365.py +++ b/dashboard/compliance/prowler_threatscore_m365.py @@ -1,6 +1,6 @@ import warnings -from dashboard.common_methods import get_section_containers_cis +from dashboard.common_methods import get_section_containers_threatscore warnings.filterwarnings("ignore") @@ -11,6 +11,7 @@ def get_table(data): "REQUIREMENTS_ID", "REQUIREMENTS_DESCRIPTION", "REQUIREMENTS_ATTRIBUTES_SECTION", + "REQUIREMENTS_ATTRIBUTES_SUBSECTION", "CHECKID", "STATUS", "REGION", @@ -19,6 +20,9 @@ def get_table(data): ] ].copy() - return get_section_containers_cis( - aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION" + return get_section_containers_threatscore( + aux, + "REQUIREMENTS_ATTRIBUTES_SECTION", + "REQUIREMENTS_ATTRIBUTES_SUBSECTION", + "REQUIREMENTS_ID", ) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 965cf81cf5..0f67503e13 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -7,9 +7,10 @@ All notable changes to the **Prowler SDK** are documented in this file. ### Added - Add CIS 1.11 compliance framework for Kubernetes. [(#7790)](https://github.com/prowler-cloud/prowler/pull/7790) - Support `HTTPS_PROXY` and `K8S_SKIP_TLS_VERIFY` in Kubernetes. [(#7720)](https://github.com/prowler-cloud/prowler/pull/7720) -- Add new check `entra_users_mfa_capable`. [(#7734)](https://github.com/prowler-cloud/prowler/pull/7734) -- Add new check `admincenter_organization_customer_lockbox_enabled`. [(#7732)](https://github.com/prowler-cloud/prowler/pull/7732) -- Add new check `admincenter_external_calendar_sharing_disabled`. [(#7733)](https://github.com/prowler-cloud/prowler/pull/7733) +- Add new check `entra_users_mfa_capable` for M365 provider. [(#7734)](https://github.com/prowler-cloud/prowler/pull/7734) +- Add new check `admincenter_organization_customer_lockbox_enabled` for M365 provider. [(#7732)](https://github.com/prowler-cloud/prowler/pull/7732) +- Add new check `admincenter_external_calendar_sharing_disabled` for M365 provider. [(#7733)](https://github.com/prowler-cloud/prowler/pull/7733) +- Add a level for Prowler ThreatScore in the accordion in Dashboard. [(#7739)](https://github.com/prowler-cloud/prowler/pull/7739) - Add CIS 4.0 compliance framework for GCP. [(7785)](https://github.com/prowler-cloud/prowler/pull/7785) ### Fixed