From 6657996169c1f793a08b95e9736de2590c6b7a31 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 8 Apr 2026 15:27:32 +0400 Subject: [PATCH 1/5] Add compliance control center dashboard Signed-off-by: tdruez --- dejacode/static/css/dejacode_bootstrap.css | 5 +- dje/templates/includes/navbar_header.html | 1 + .../includes/navbar_header_tools_menu.html | 6 + .../compliance_dashboard.html | 149 ++++++++++++++++++ product_portfolio/urls.py | 6 + product_portfolio/views.py | 110 +++++++++++++ 6 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 product_portfolio/templates/product_portfolio/compliance_dashboard.html diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index 91ba8de9..1c76c4ab 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -94,11 +94,14 @@ table.text-break thead { } .bg-warning-orange { background-color: var(--bs-orange); - color: #000; + color: #fff; } .text-warning-orange { color: var(--bs-orange) !important; } +.bg-warning-orange-subtle { + background-color: rgba(253, 126, 20, 0.15); +} .spinner-border-md { --bs-spinner-width: 1.5rem; --bs-spinner-height: 1.5rem; diff --git a/dje/templates/includes/navbar_header.html b/dje/templates/includes/navbar_header.html index af0dc424..405aeedd 100644 --- a/dje/templates/includes/navbar_header.html +++ b/dje/templates/includes/navbar_header.html @@ -6,6 +6,7 @@ {% url 'license_library:license_list' as license_list_url %} {% url 'organization:owner_list' as owner_list_url %} {% url 'global_search' as global_search_url %} +{% url 'product_portfolio:compliance_dashboard' as compliance_dashboard_url %} {% url 'reporting:report_list' as report_list_url %} {% url 'workflow:request_list' as request_list_url %} {% url 'component_catalog:scan_list' as scan_list_url %} diff --git a/dje/templates/includes/navbar_header_tools_menu.html b/dje/templates/includes/navbar_header_tools_menu.html index 6bd6440a..ced30d5c 100644 --- a/dje/templates/includes/navbar_header_tools_menu.html +++ b/dje/templates/includes/navbar_header_tools_menu.html @@ -5,6 +5,12 @@ Tools
-
+
-
{% trans "Products" %}
-
- {{ products_ok }} / {{ total_products }} +
{% trans "Products with issues" %}
+
+ {{ products_with_issues }}
- {% trans "active products with no issues" %} + {% if products_with_issues %} + {% trans "of" %} {{ total_products }} {% trans "active products" %} + {% else %} + {% trans "All" %} {{ total_products }} {% trans "products are compliant" %} + {% endif %}
@@ -38,30 +42,30 @@

{% trans "Compliance Control Center" %}

-
{% trans "Security compliance" %}
-
- {{ security_compliance_pct }}% +
{% trans "Security issues" %}
+
+ {{ products_with_critical_or_high }}
- {% if products_security_ok == total_products %} - {% trans "No critical or high vulnerabilities" %} + {% if products_with_critical_or_high %} + {% trans "products with critical/high vulnerabilities" %} {% else %} - {{ products_security_ok }} {% trans "of" %} {{ total_products }} {% trans "products without critical/high" %} + {% trans "No critical or high vulnerabilities" %} {% endif %}
-
{% trans "Vulnerabilities" %}
-
- {{ products_with_vulnerabilities }} +
{% trans "Total vulnerabilities" %}
+
+ {{ total_vulnerabilities|intcomma }}
- {% if products_with_critical %} - {{ products_with_critical }} {% trans "with critical vulnerabilities" %} - {% elif products_with_vulnerabilities %} - {% trans "products with vulnerabilities" %} + {% if total_critical %} + {{ total_critical }} {% trans "critical" %}{% if total_high %}, {{ total_high }} {% trans "high" %}{% endif %} + {% elif total_vulnerabilities %} + {% trans "across all products" %} {% else %} {% trans "No known vulnerabilities" %} {% endif %} @@ -70,7 +74,7 @@

{% trans "Compliance Control Center" %}

-
+
@@ -131,7 +135,7 @@

{% trans "Compliance Control Center" %}

{{ product.high_count }} {% trans "high" %} {% endif %} {% if not product.critical_count and not product.high_count %} - {{ product.vulnerability_count }} + {{ product.vulnerability_count }} {% endif %} diff --git a/product_portfolio/views.py b/product_portfolio/views.py index e01efc50..a42cf032 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -2884,42 +2884,43 @@ def get_queryset(self): ) def get_context_data(self, **kwargs): + from django.db.models import Sum + context = super().get_context_data(**kwargs) products = self.object_list total_products = products.count() - products_with_license_issues = products.filter( - Q(license_error_count__gt=0) | Q(license_warning_count__gt=0) + products_with_issues = products.filter( + Q(license_error_count__gt=0) + | Q(license_warning_count__gt=0) + | Q(critical_count__gt=0) + | Q(high_count__gt=0) ).count() - products_with_vulnerabilities = products.filter(vulnerability_count__gt=0).count() - - products_with_critical = products.filter(critical_count__gt=0).count() - - products_ok = products.filter( - license_error_count=0, - license_warning_count=0, - vulnerability_count=0, + products_with_license_issues = products.filter( + Q(license_error_count__gt=0) | Q(license_warning_count__gt=0) ).count() - products_security_ok = products.filter( - Q(vulnerability_count=0) | Q(critical_count=0, high_count=0) + products_with_critical_or_high = products.filter( + Q(critical_count__gt=0) | Q(high_count__gt=0) ).count() - security_compliance_pct = ( - round((products_security_ok / total_products) * 100) if total_products else 100 + totals = products.aggregate( + total_vulnerabilities=Sum("vulnerability_count"), + total_critical=Sum("critical_count"), + total_high=Sum("high_count"), ) context.update( { "total_products": total_products, + "products_with_issues": products_with_issues, "products_with_license_issues": products_with_license_issues, - "products_with_vulnerabilities": products_with_vulnerabilities, - "products_with_critical": products_with_critical, - "products_ok": products_ok, - "products_security_ok": products_security_ok, - "security_compliance_pct": security_compliance_pct, + "products_with_critical_or_high": products_with_critical_or_high, + "total_vulnerabilities": totals["total_vulnerabilities"] or 0, + "total_critical": totals["total_critical"] or 0, + "total_high": totals["total_high"] or 0, } ) From 5602234ada660170f2db1b78abc988202880ec39 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 8 Apr 2026 17:51:05 +0400 Subject: [PATCH 3/5] Refine UI and CSS Signed-off-by: tdruez --- dejacode/static/css/dejacode_bootstrap.css | 3 +-- .../product_portfolio/compliance_dashboard.html | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index 1c76c4ab..f49cb19d 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -801,8 +801,7 @@ pre.log { .nav-pills .show>.nav-link { background-color: var(--bs-djc-blue-bg); } -.card, -.table { +.card { box-shadow: rgba(0, 0, 0, 0.05) 0 0.0625rem 0.125rem; } .table-md th, diff --git a/product_portfolio/templates/product_portfolio/compliance_dashboard.html b/product_portfolio/templates/product_portfolio/compliance_dashboard.html index ffee61a8..c34841fa 100644 --- a/product_portfolio/templates/product_portfolio/compliance_dashboard.html +++ b/product_portfolio/templates/product_portfolio/compliance_dashboard.html @@ -78,22 +78,22 @@

{% trans "Compliance Control Center" %}

- - - - - + + + + + {% for product in object_list %} {% with product_url=product.get_absolute_url %} -
{% trans "Product" %}{% trans "Packages" %}{% trans "License compliance" %}{% trans "Security compliance" %}{% trans "Vulnerabilities" %}{% trans "Product" %}{% trans "Packages" %}{% trans "License compliance" %}{% trans "Security compliance" %}{% trans "Vulnerabilities" %}
+ {{ product }} - + {{ product.package_count|intcomma }} From 7ab452e232aba6dd12de8e83669c138feb29d4a2 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 8 Apr 2026 18:09:44 +0400 Subject: [PATCH 4/5] Improve card values Signed-off-by: tdruez --- .../compliance_dashboard.html | 22 +++++++----- product_portfolio/views.py | 36 ++++++++++++++----- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/product_portfolio/templates/product_portfolio/compliance_dashboard.html b/product_portfolio/templates/product_portfolio/compliance_dashboard.html index c34841fa..98c156dd 100644 --- a/product_portfolio/templates/product_portfolio/compliance_dashboard.html +++ b/product_portfolio/templates/product_portfolio/compliance_dashboard.html @@ -63,7 +63,7 @@

{% trans "Compliance Control Center" %}

{% if total_critical %} - {{ total_critical }} {% trans "critical" %}{% if total_high %}, {{ total_high }} {% trans "high" %}{% endif %} + {{ total_critical }} {% trans "critical" %}{% if total_high %}, {{ total_high }} {% trans "high" %}{% endif %}{% if total_medium %}, {{ total_medium }} {% trans "medium" %}{% endif %}{% if total_low %}, {{ total_low }} {% trans "low" %}{% endif %} {% elif total_vulnerabilities %} {% trans "across all products" %} {% else %} @@ -78,11 +78,11 @@

{% trans "Compliance Control Center" %}

- - - - - + + + + + @@ -134,8 +134,14 @@

{% trans "Compliance Control Center" %}

{% if product.high_count %} {{ product.high_count }} {% trans "high" %} {% endif %} - {% if not product.critical_count and not product.high_count %} - {{ product.vulnerability_count }} + {% if product.medium_count %} + {{ product.medium_count }} {% trans "medium" %} + {% endif %} + {% if product.low_count %} + {{ product.low_count }} {% trans "low" %} + {% endif %} + {% if not product.vulnerability_count %} + {% trans "None" %} {% endif %} diff --git a/product_portfolio/views.py b/product_portfolio/views.py index a42cf032..2c637069 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -2845,6 +2845,14 @@ def get_queryset(self): distinct=True, ), max_risk_score=Max("productpackages__package__affected_by_vulnerabilities__risk_score"), + max_risk_level=Case( + When(max_risk_score__gte=8.0, then=Value("critical")), + When(max_risk_score__gte=6.0, then=Value("high")), + When(max_risk_score__gte=3.0, then=Value("medium")), + When(max_risk_score__gte=0.1, then=Value("low")), + default=Value(""), + output_field=CharField(max_length=8), + ), critical_count=Count( "productpackages__package__affected_by_vulnerabilities", filter=Q( @@ -2857,6 +2865,20 @@ def get_queryset(self): filter=Q(productpackages__package__affected_by_vulnerabilities__risk_level="high"), distinct=True, ), + medium_count=Count( + "productpackages__package__affected_by_vulnerabilities", + filter=Q( + productpackages__package__affected_by_vulnerabilities__risk_level="medium" + ), + distinct=True, + ), + low_count=Count( + "productpackages__package__affected_by_vulnerabilities", + filter=Q( + productpackages__package__affected_by_vulnerabilities__risk_level="low" + ), + distinct=True, + ), license_warning_count=Count( "productpackages__licenses", filter=Q(productpackages__licenses__usage_policy__compliance_alert="warning"), @@ -2867,20 +2889,12 @@ def get_queryset(self): filter=Q(productpackages__licenses__usage_policy__compliance_alert="error"), distinct=True, ), - max_risk_level=Case( - When(max_risk_score__gte=8.0, then=Value("critical")), - When(max_risk_score__gte=6.0, then=Value("high")), - When(max_risk_score__gte=3.0, then=Value("medium")), - When(max_risk_score__gte=0.1, then=Value("low")), - default=Value(""), - output_field=CharField(max_length=8), - ), ).order_by( F("max_risk_score").desc(nulls_last=True), "-license_error_count", "-license_warning_count", "name", - "version", + "-version", ) def get_context_data(self, **kwargs): @@ -2910,6 +2924,8 @@ def get_context_data(self, **kwargs): total_vulnerabilities=Sum("vulnerability_count"), total_critical=Sum("critical_count"), total_high=Sum("high_count"), + total_medium=Sum("medium_count"), + total_low=Sum("low_count"), ) context.update( @@ -2921,6 +2937,8 @@ def get_context_data(self, **kwargs): "total_vulnerabilities": totals["total_vulnerabilities"] or 0, "total_critical": totals["total_critical"] or 0, "total_high": totals["total_high"] or 0, + "total_medium": totals["total_medium"] or 0, + "total_low": totals["total_low"] or 0, } ) From 00295f59c8139ef4f4f82e10e8d07c3f5ebfb53b Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 8 Apr 2026 18:23:15 +0400 Subject: [PATCH 5/5] Fix code format Signed-off-by: tdruez --- product_portfolio/views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 2c637069..215417d9 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -2874,9 +2874,7 @@ def get_queryset(self): ), low_count=Count( "productpackages__package__affected_by_vulnerabilities", - filter=Q( - productpackages__package__affected_by_vulnerabilities__risk_level="low" - ), + filter=Q(productpackages__package__affected_by_vulnerabilities__risk_level="low"), distinct=True, ), license_warning_count=Count(
{% trans "Product" %}{% trans "Packages" %}{% trans "License compliance" %}{% trans "Security compliance" %}{% trans "Vulnerabilities" %}{% trans "Product" %}{% trans "Packages" %}{% trans "License compliance" %}{% trans "Security compliance" %}{% trans "Vulnerabilities" %}