Skip to content

Commit 6657996

Browse files
committed
Add compliance control center dashboard
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 3fa838d commit 6657996

6 files changed

Lines changed: 276 additions & 1 deletion

File tree

dejacode/static/css/dejacode_bootstrap.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,14 @@ table.text-break thead {
9494
}
9595
.bg-warning-orange {
9696
background-color: var(--bs-orange);
97-
color: #000;
97+
color: #fff;
9898
}
9999
.text-warning-orange {
100100
color: var(--bs-orange) !important;
101101
}
102+
.bg-warning-orange-subtle {
103+
background-color: rgba(253, 126, 20, 0.15);
104+
}
102105
.spinner-border-md {
103106
--bs-spinner-width: 1.5rem;
104107
--bs-spinner-height: 1.5rem;

dje/templates/includes/navbar_header.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
{% url 'license_library:license_list' as license_list_url %}
77
{% url 'organization:owner_list' as owner_list_url %}
88
{% url 'global_search' as global_search_url %}
9+
{% url 'product_portfolio:compliance_dashboard' as compliance_dashboard_url %}
910
{% url 'reporting:report_list' as report_list_url %}
1011
{% url 'workflow:request_list' as request_list_url %}
1112
{% url 'component_catalog:scan_list' as scan_list_url %}

dje/templates/includes/navbar_header_tools_menu.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
Tools
66
</a>
77
<div class="dropdown-menu">
8+
<div class="dropdown-header">Compliance</div>
9+
<a class="dropdown-item{% if compliance_dashboard_url in request.path %} active{% endif %}" href="{{ compliance_dashboard_url }}">
10+
<i class="fa-solid fa-shield-halved" aria-hidden="true"></i>
11+
Control Center
12+
</a>
13+
<div class="dropdown-divider"></div>
814
<div class="dropdown-header">Reporting</div>
915
<a class="dropdown-item{% if report_list_url in request.path %} active{% endif %}" href="{{ report_list_url }}">
1016
<i class="far fa-chart-bar" aria-hidden="true"></i>
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
{% extends "bootstrap_base.html" %}
2+
{% load i18n humanize %}
3+
4+
{% block page_title %}{% trans "Compliance Control Center" %}{% endblock %}
5+
6+
{% block content %}
7+
<div class="d-flex align-items-baseline gap-3 mb-3">
8+
<h1 class="h3 mb-0">{% trans "Compliance Control Center" %}</h1>
9+
<span class="text-body-secondary">{{ total_products }} {% trans "active product" %}{{ total_products|pluralize }}</span>
10+
</div>
11+
12+
<div class="row g-3 mb-3">
13+
<div class="col-6 col-md-3">
14+
<div class="bg-body-secondary rounded-3 p-3">
15+
<div class="small text-body-secondary mb-1">{% trans "Products" %}</div>
16+
<div class="fs-4 fw-medium lh-sm {% if products_ok == total_products %}text-success{% else %}text-warning-orange{% endif %}">
17+
{{ products_ok }} / {{ total_products }}
18+
</div>
19+
<div class="text-body-tertiary fs-xs mt-1">
20+
{% trans "active products with no issues" %}
21+
</div>
22+
</div>
23+
</div>
24+
<div class="col-6 col-md-3">
25+
<div class="bg-body-secondary rounded-3 p-3">
26+
<div class="small text-body-secondary mb-1">{% trans "License issues" %}</div>
27+
<div class="fs-4 fw-medium lh-sm {% if products_with_license_issues %}text-danger{% else %}text-success{% endif %}">
28+
{{ products_with_license_issues }}
29+
</div>
30+
<div class="text-body-tertiary fs-xs mt-1">
31+
{% if products_with_license_issues %}
32+
{% trans "products with policy violations" %}
33+
{% else %}
34+
{% trans "All products within policy" %}
35+
{% endif %}
36+
</div>
37+
</div>
38+
</div>
39+
<div class="col-6 col-md-3">
40+
<div class="bg-body-secondary rounded-3 p-3">
41+
<div class="small text-body-secondary mb-1">{% trans "Security compliance" %}</div>
42+
<div class="fs-4 fw-medium lh-sm {% if security_compliance_pct == 100 %}text-success{% elif security_compliance_pct >= 90 %}text-warning-orange{% else %}text-danger{% endif %}">
43+
{{ security_compliance_pct }}%
44+
</div>
45+
<div class="text-body-tertiary fs-xs mt-1">
46+
{% if products_security_ok == total_products %}
47+
{% trans "No critical or high vulnerabilities" %}
48+
{% else %}
49+
{{ products_security_ok }} {% trans "of" %} {{ total_products }} {% trans "products without critical/high" %}
50+
{% endif %}
51+
</div>
52+
</div>
53+
</div>
54+
<div class="col-6 col-md-3">
55+
<div class="bg-body-secondary rounded-3 p-3">
56+
<div class="small text-body-secondary mb-1">{% trans "Vulnerabilities" %}</div>
57+
<div class="fs-4 fw-medium lh-sm {% if products_with_critical %}text-danger{% elif products_with_vulnerabilities %}text-warning-orange{% else %}text-success{% endif %}">
58+
{{ products_with_vulnerabilities }}
59+
</div>
60+
<div class="text-body-tertiary fs-xs mt-1">
61+
{% if products_with_critical %}
62+
{{ products_with_critical }} {% trans "with critical vulnerabilities" %}
63+
{% elif products_with_vulnerabilities %}
64+
{% trans "products with vulnerabilities" %}
65+
{% else %}
66+
{% trans "No known vulnerabilities" %}
67+
{% endif %}
68+
</div>
69+
</div>
70+
</div>
71+
</div>
72+
73+
<div class="border rounded-3 p-3">
74+
<table class="table table-sm mb-0">
75+
<thead>
76+
<tr>
77+
<th class="fw-medium">{% trans "Product" %}</th>
78+
<th class="fw-medium text-end">{% trans "Packages" %}</th>
79+
<th class="fw-medium text-end">{% trans "License compliance" %}</th>
80+
<th class="fw-medium text-end">{% trans "Security compliance" %}</th>
81+
<th class="fw-medium text-end">{% trans "Vulnerabilities" %}</th>
82+
</tr>
83+
</thead>
84+
<tbody>
85+
{% for product in object_list %}
86+
{% with product_url=product.get_absolute_url %}
87+
<tr>
88+
<td>
89+
<a href="{{ product_url }}#compliance">
90+
{{ product }}
91+
</a>
92+
</td>
93+
<td class="text-end">
94+
<a href="{{ product_url }}#inventory">
95+
{{ product.package_count|intcomma }}
96+
</a>
97+
</td>
98+
<td class="text-end">
99+
{% if product.license_error_count %}
100+
<span class="badge bg-danger-subtle text-danger-emphasis">
101+
{{ product.license_error_count }} {% trans "error" %}{{ product.license_error_count|pluralize }}
102+
</span>
103+
{% endif %}
104+
{% if product.license_warning_count %}
105+
<span class="badge bg-warning-subtle text-warning-emphasis ms-1">
106+
{{ product.license_warning_count }} {% trans "warning" %}{{ product.license_warning_count|pluralize }}
107+
</span>
108+
{% endif %}
109+
{% if not product.license_error_count and not product.license_warning_count %}
110+
<span class="badge bg-success-subtle text-success-emphasis">{% trans "OK" %}</span>
111+
{% endif %}
112+
</td>
113+
<td class="text-end">
114+
{% if product.max_risk_level == "critical" %}
115+
<span class="badge bg-danger-subtle text-danger-emphasis">{% trans "Critical" %}</span>
116+
{% elif product.max_risk_level == "high" %}
117+
<span class="badge bg-warning-orange-subtle text-warning-orange">{% trans "High" %}</span>
118+
{% elif product.max_risk_level == "medium" %}
119+
<span class="badge bg-warning-subtle text-warning-emphasis">{% trans "Medium" %}</span>
120+
{% elif product.max_risk_level == "low" %}
121+
<span class="badge bg-info-subtle text-info-emphasis">{% trans "Low" %}</span>
122+
{% else %}
123+
<span class="badge bg-success-subtle text-success-emphasis">{% trans "OK" %}</span>
124+
{% endif %}
125+
</td>
126+
<td class="text-end">
127+
{% if product.critical_count %}
128+
<span class="badge bg-danger-subtle text-danger-emphasis">{{ product.critical_count }} {% trans "critical" %}</span>
129+
{% endif %}
130+
{% if product.high_count %}
131+
<span class="badge bg-warning-orange-subtle text-warning-orange ms-1">{{ product.high_count }} {% trans "high" %}</span>
132+
{% endif %}
133+
{% if not product.critical_count and not product.high_count %}
134+
<span class="text-body-secondary small">{{ product.vulnerability_count }}</span>
135+
{% endif %}
136+
</td>
137+
</tr>
138+
{% endwith %}
139+
{% empty %}
140+
<tr>
141+
<td colspan="5" class="text-center text-body-tertiary py-4">
142+
{% trans "No active products" %}
143+
</td>
144+
</tr>
145+
{% endfor %}
146+
</tbody>
147+
</table>
148+
</div>
149+
{% endblock %}

product_portfolio/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.urls import path
1010

1111
from product_portfolio.views import AttributionView
12+
from product_portfolio.views import ComplianceDashboardView
1213
from product_portfolio.views import ImportManifestsView
1314
from product_portfolio.views import LoadSBOMsView
1415
from product_portfolio.views import ManageComponentGridView
@@ -65,6 +66,11 @@ def product_path(path_segment, view):
6566

6667

6768
urlpatterns = [
69+
path(
70+
"compliance_dashboard/",
71+
ComplianceDashboardView.as_view(),
72+
name="compliance_dashboard",
73+
),
6874
path(
6975
"import_packages_from_scancodeio/<str:key>/",
7076
import_packages_from_scancodeio_view,

product_portfolio/views.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2814,3 +2814,113 @@ def get_security_compliance_context(product, display_limit=10):
28142814
"vulnerabilities": all_vulnerabilities_ordered[:display_limit],
28152815
**severity_counts,
28162816
}
2817+
2818+
2819+
class ComplianceDashboardView(LoginRequiredMixin, DataspacedFilterView):
2820+
"""Compliance control center: overview of all products."""
2821+
2822+
template_name = "product_portfolio/compliance_dashboard.html"
2823+
model = Product
2824+
filterset_class = ProductFilterSet
2825+
paginate_by = 50
2826+
2827+
def get_queryset(self):
2828+
from django.db.models import Case
2829+
from django.db.models import CharField
2830+
from django.db.models import Max
2831+
from django.db.models import Value
2832+
from django.db.models import When
2833+
2834+
base_qs = Product.objects.get_queryset(
2835+
user=self.request.user,
2836+
perms="view_product",
2837+
include_inactive=False,
2838+
exclude_locked=True,
2839+
)
2840+
2841+
return base_qs.annotate(
2842+
package_count=Count("productpackages", distinct=True),
2843+
vulnerability_count=Count(
2844+
"productpackages__package__affected_by_vulnerabilities",
2845+
distinct=True,
2846+
),
2847+
max_risk_score=Max("productpackages__package__affected_by_vulnerabilities__risk_score"),
2848+
critical_count=Count(
2849+
"productpackages__package__affected_by_vulnerabilities",
2850+
filter=Q(
2851+
productpackages__package__affected_by_vulnerabilities__risk_level="critical"
2852+
),
2853+
distinct=True,
2854+
),
2855+
high_count=Count(
2856+
"productpackages__package__affected_by_vulnerabilities",
2857+
filter=Q(productpackages__package__affected_by_vulnerabilities__risk_level="high"),
2858+
distinct=True,
2859+
),
2860+
license_warning_count=Count(
2861+
"productpackages__licenses",
2862+
filter=Q(productpackages__licenses__usage_policy__compliance_alert="warning"),
2863+
distinct=True,
2864+
),
2865+
license_error_count=Count(
2866+
"productpackages__licenses",
2867+
filter=Q(productpackages__licenses__usage_policy__compliance_alert="error"),
2868+
distinct=True,
2869+
),
2870+
max_risk_level=Case(
2871+
When(max_risk_score__gte=8.0, then=Value("critical")),
2872+
When(max_risk_score__gte=6.0, then=Value("high")),
2873+
When(max_risk_score__gte=3.0, then=Value("medium")),
2874+
When(max_risk_score__gte=0.1, then=Value("low")),
2875+
default=Value(""),
2876+
output_field=CharField(max_length=8),
2877+
),
2878+
).order_by(
2879+
F("max_risk_score").desc(nulls_last=True),
2880+
"-license_error_count",
2881+
"-license_warning_count",
2882+
"name",
2883+
"version",
2884+
)
2885+
2886+
def get_context_data(self, **kwargs):
2887+
context = super().get_context_data(**kwargs)
2888+
2889+
products = self.object_list
2890+
total_products = products.count()
2891+
2892+
products_with_license_issues = products.filter(
2893+
Q(license_error_count__gt=0) | Q(license_warning_count__gt=0)
2894+
).count()
2895+
2896+
products_with_vulnerabilities = products.filter(vulnerability_count__gt=0).count()
2897+
2898+
products_with_critical = products.filter(critical_count__gt=0).count()
2899+
2900+
products_ok = products.filter(
2901+
license_error_count=0,
2902+
license_warning_count=0,
2903+
vulnerability_count=0,
2904+
).count()
2905+
2906+
products_security_ok = products.filter(
2907+
Q(vulnerability_count=0) | Q(critical_count=0, high_count=0)
2908+
).count()
2909+
2910+
security_compliance_pct = (
2911+
round((products_security_ok / total_products) * 100) if total_products else 100
2912+
)
2913+
2914+
context.update(
2915+
{
2916+
"total_products": total_products,
2917+
"products_with_license_issues": products_with_license_issues,
2918+
"products_with_vulnerabilities": products_with_vulnerabilities,
2919+
"products_with_critical": products_with_critical,
2920+
"products_ok": products_ok,
2921+
"products_security_ok": products_security_ok,
2922+
"security_compliance_pct": security_compliance_pct,
2923+
}
2924+
)
2925+
2926+
return context

0 commit comments

Comments
 (0)