Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions dejacode/static/css/dejacode_bootstrap.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -798,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,
Expand Down
1 change: 1 addition & 0 deletions dje/templates/includes/navbar_header.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
6 changes: 6 additions & 0 deletions dje/templates/includes/navbar_header_tools_menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
Tools
</a>
<div class="dropdown-menu">
<div class="dropdown-header">Compliance</div>
<a class="dropdown-item{% if compliance_dashboard_url in request.path %} active{% endif %}" href="{{ compliance_dashboard_url }}">
<i class="fa-solid fa-shield-halved" aria-hidden="true"></i>
Control Center
</a>
<div class="dropdown-divider"></div>
<div class="dropdown-header">Reporting</div>
<a class="dropdown-item{% if report_list_url in request.path %} active{% endif %}" href="{{ report_list_url }}">
<i class="far fa-chart-bar" aria-hidden="true"></i>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
{% extends "bootstrap_base.html" %}
{% load i18n humanize %}

{% block page_title %}{% trans "Compliance Control Center" %}{% endblock %}

{% block content %}
<div class="d-flex align-items-baseline gap-3 mb-3">
<h1 class="h3 mb-0">{% trans "Compliance Control Center" %}</h1>
<span class="text-body-secondary">{{ total_products }} {% trans "active product" %}{{ total_products|pluralize }}</span>
</div>

<div class="row g-3 mb-3">
<div class="col-6 col-md-3">
<div class="bg-body-secondary rounded-3 p-3">
<div class="small text-body-secondary mb-1">{% trans "Products with issues" %}</div>
<div class="fs-4 fw-medium lh-sm {% if products_with_issues %}text-danger{% else %}text-success{% endif %}">
{{ products_with_issues }}
</div>
<div class="text-body-tertiary fs-xs mt-1">
{% if products_with_issues %}
{% trans "of" %} {{ total_products }} {% trans "active products" %}
{% else %}
{% trans "All" %} {{ total_products }} {% trans "products are compliant" %}
{% endif %}
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="bg-body-secondary rounded-3 p-3">
<div class="small text-body-secondary mb-1">{% trans "License issues" %}</div>
<div class="fs-4 fw-medium lh-sm {% if products_with_license_issues %}text-danger{% else %}text-success{% endif %}">
{{ products_with_license_issues }}
</div>
<div class="text-body-tertiary fs-xs mt-1">
{% if products_with_license_issues %}
{% trans "products with policy violations" %}
{% else %}
{% trans "All products within policy" %}
{% endif %}
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="bg-body-secondary rounded-3 p-3">
<div class="small text-body-secondary mb-1">{% trans "Security issues" %}</div>
<div class="fs-4 fw-medium lh-sm {% if products_with_critical_or_high %}text-danger{% elif products_with_vulnerabilities %}text-warning-orange{% else %}text-success{% endif %}">
{{ products_with_critical_or_high }}
</div>
<div class="text-body-tertiary fs-xs mt-1">
{% if products_with_critical_or_high %}
{% trans "products with critical/high vulnerabilities" %}
{% else %}
{% trans "No critical or high vulnerabilities" %}
{% endif %}
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="bg-body-secondary rounded-3 p-3">
<div class="small text-body-secondary mb-1">{% trans "Total vulnerabilities" %}</div>
<div class="fs-4 fw-medium lh-sm {% if total_critical %}text-danger{% elif total_vulnerabilities %}text-warning-orange{% else %}text-success{% endif %}">
{{ total_vulnerabilities|intcomma }}
</div>
<div class="text-body-tertiary fs-xs mt-1">
{% if total_critical %}
{{ 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 %}
{% trans "No known vulnerabilities" %}
{% endif %}
</div>
</div>
</div>
</div>

<div class="border rounded-3 p-3 pt-2">
<table class="table table-sm mb-0">
<thead>
<tr>
<th class="fw-medium">{% trans "Product" %}</th>
<th class="fw-medium text-end">{% trans "Packages" %}</th>
<th class="fw-medium text-end">{% trans "License compliance" %}</th>
<th class="fw-medium text-end">{% trans "Security compliance" %}</th>
<th class="fw-medium text-end">{% trans "Vulnerabilities" %}</th>
</tr>
</thead>
<tbody>
{% for product in object_list %}
{% with product_url=product.get_absolute_url %}
<tr>
<th class="align-middle">
<a href="{{ product_url }}#compliance">
{{ product }}
</a>
</th>
<td class="text-end">
<a href="{{ product_url }}#inventory">
{{ product.package_count|intcomma }}
</a>
</td>
<td class="text-end">
{% if product.license_error_count %}
<span class="badge bg-danger-subtle text-danger-emphasis">
{{ product.license_error_count }} {% trans "error" %}{{ product.license_error_count|pluralize }}
</span>
{% endif %}
{% if product.license_warning_count %}
<span class="badge bg-warning-subtle text-warning-emphasis ms-1">
{{ product.license_warning_count }} {% trans "warning" %}{{ product.license_warning_count|pluralize }}
</span>
{% endif %}
{% if not product.license_error_count and not product.license_warning_count %}
<span class="badge bg-success-subtle text-success-emphasis">{% trans "OK" %}</span>
{% endif %}
</td>
<td class="text-end">
{% if product.max_risk_level == "critical" %}
<span class="badge bg-danger-subtle text-danger-emphasis">{% trans "Critical" %}</span>
{% elif product.max_risk_level == "high" %}
<span class="badge bg-warning-orange-subtle text-warning-orange">{% trans "High" %}</span>
{% elif product.max_risk_level == "medium" %}
<span class="badge bg-warning-subtle text-warning-emphasis">{% trans "Medium" %}</span>
{% elif product.max_risk_level == "low" %}
<span class="badge bg-info-subtle text-info-emphasis">{% trans "Low" %}</span>
{% else %}
<span class="badge bg-success-subtle text-success-emphasis">{% trans "OK" %}</span>
{% endif %}
</td>
<td class="text-end">
{% if product.critical_count %}
<span class="badge bg-danger-subtle text-danger-emphasis">{{ product.critical_count }} {% trans "critical" %}</span>
{% endif %}
{% if product.high_count %}
<span class="badge bg-warning-orange-subtle text-warning-orange ms-1">{{ product.high_count }} {% trans "high" %}</span>
{% endif %}
{% if product.medium_count %}
<span class="badge bg-warning-subtle text-warning-emphasis ms-1">{{ product.medium_count }} {% trans "medium" %}</span>
{% endif %}
{% if product.low_count %}
<span class="badge bg-info-subtle text-info-emphasis ms-1">{{ product.low_count }} {% trans "low" %}</span>
{% endif %}
{% if not product.vulnerability_count %}
<span class="text-body-tertiary small">{% trans "None" %}</span>
{% endif %}
</td>
</tr>
{% endwith %}
{% empty %}
<tr>
<td colspan="5" class="text-center text-body-tertiary py-4">
{% trans "No active products" %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
6 changes: 6 additions & 0 deletions product_portfolio/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.urls import path

from product_portfolio.views import AttributionView
from product_portfolio.views import ComplianceDashboardView
from product_portfolio.views import ImportManifestsView
from product_portfolio.views import LoadSBOMsView
from product_portfolio.views import ManageComponentGridView
Expand Down Expand Up @@ -65,6 +66,11 @@ def product_path(path_segment, view):


urlpatterns = [
path(
"compliance_dashboard/",
ComplianceDashboardView.as_view(),
name="compliance_dashboard",
),
path(
"import_packages_from_scancodeio/<str:key>/",
import_packages_from_scancodeio_view,
Expand Down
127 changes: 127 additions & 0 deletions product_portfolio/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2814,3 +2814,130 @@ def get_security_compliance_context(product, display_limit=10):
"vulnerabilities": all_vulnerabilities_ordered[:display_limit],
**severity_counts,
}


class ComplianceDashboardView(LoginRequiredMixin, DataspacedFilterView):
"""Compliance control center: overview of all products."""

template_name = "product_portfolio/compliance_dashboard.html"
model = Product
filterset_class = ProductFilterSet
paginate_by = 50

def get_queryset(self):
from django.db.models import Case
from django.db.models import CharField
from django.db.models import Max
from django.db.models import Value
from django.db.models import When

base_qs = Product.objects.get_queryset(
user=self.request.user,
perms="view_product",
include_inactive=False,
exclude_locked=True,
)

return base_qs.annotate(
package_count=Count("productpackages", distinct=True),
vulnerability_count=Count(
"productpackages__package__affected_by_vulnerabilities",
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(
productpackages__package__affected_by_vulnerabilities__risk_level="critical"
),
distinct=True,
),
high_count=Count(
"productpackages__package__affected_by_vulnerabilities",
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"),
distinct=True,
),
license_error_count=Count(
"productpackages__licenses",
filter=Q(productpackages__licenses__usage_policy__compliance_alert="error"),
distinct=True,
),
).order_by(
F("max_risk_score").desc(nulls_last=True),
"-license_error_count",
"-license_warning_count",
"name",
"-version",
)

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_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_license_issues = products.filter(
Q(license_error_count__gt=0) | Q(license_warning_count__gt=0)
).count()

products_with_critical_or_high = products.filter(
Q(critical_count__gt=0) | Q(high_count__gt=0)
).count()

totals = products.aggregate(
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(
{
"total_products": total_products,
"products_with_issues": products_with_issues,
"products_with_license_issues": products_with_license_issues,
"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,
"total_medium": totals["total_medium"] or 0,
"total_low": totals["total_low"] or 0,
}
)

return context
Loading