Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8824bc1
add licenses filter on BaseProductRelationFilterSet
tdruez Mar 23, 2026
c2055a7
add a get_risk_score_label function
tdruez Mar 23, 2026
333981a
prototype of the product compliance dashboard
tdruez Mar 23, 2026
6bff6d2
move the product compliance dashboard to a tab
tdruez Mar 23, 2026
419cf9f
refactor the tab context
tdruez Mar 23, 2026
8730487
refine the vulnerability panel context
tdruez Mar 23, 2026
20e23fa
refine the metric cards
tdruez Mar 23, 2026
569cc5d
refine the license coverage metric card
tdruez Mar 23, 2026
d314951
refine the license compliance metric card
tdruez Mar 23, 2026
154e4e5
refine the license compliance panel
tdruez Mar 23, 2026
f37708f
fix the security panel
tdruez Mar 23, 2026
e24edd5
refine vulnerability display
tdruez Mar 24, 2026
2c04556
do not show tabs until packages are assigned
tdruez Mar 24, 2026
8c9fb35
fix ordering by higher risk
tdruez Mar 24, 2026
790ddd3
fix color rendering
tdruez Mar 24, 2026
90086b2
fix the counts
tdruez Mar 24, 2026
979002c
add inventory filters
tdruez Mar 24, 2026
43e90b3
add links to filtered inventory
tdruez Mar 24, 2026
f339dd9
fix code validity
tdruez Mar 24, 2026
358de4a
display filter labels and fix colors
tdruez Mar 24, 2026
ee5af4f
cleanup the view
tdruez Mar 25, 2026
ffd6baa
add distinct on compliance filters
tdruez Mar 25, 2026
89de3e5
fix failing tests
tdruez Mar 25, 2026
c30148f
refine thew view
tdruez Mar 25, 2026
8e476cd
fix template
tdruez Mar 25, 2026
90a334d
add risk_level generate field
tdruez Mar 25, 2026
917114a
Merge branch 'main' into 5c-product-compliance
tdruez Mar 26, 2026
db6fffc
cleanup
tdruez Mar 26, 2026
a948895
add unit tests
tdruez Mar 26, 2026
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
2 changes: 1 addition & 1 deletion component_catalog/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@
from license_library.models import License
from organization.api import OwnerEmbeddedSerializer
from vulnerabilities.api import VulnerabilitySerializer
from vulnerabilities.filters import RISK_SCORE_RANGES
from vulnerabilities.filters import ScoreRangeFilter
from vulnerabilities.models import RISK_SCORE_RANGES


class LicenseSummaryMixin:
Expand Down
7 changes: 7 additions & 0 deletions dejacode/static/css/dejacode_bootstrap.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ a.dropdown-item:hover {
.fs-110pct {
font-size: 110%;
}
.fs-xs {
font-size: .75rem;
}
.header {
margin-bottom: 1rem;
}
Expand Down Expand Up @@ -91,6 +94,10 @@ table.text-break thead {
}
.bg-warning-orange {
background-color: var(--bs-orange);
color: #000;
}
.text-warning-orange {
color: var(--bs-orange) !important;
}
.spinner-border-md {
--bs-spinner-width: 1.5rem;
Expand Down
1 change: 1 addition & 0 deletions dje/tests/test_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def test_permissions_get_all_tabsets(self):
],
"product": [
"essentials",
"compliance",
"inventory",
"codebase",
"hierarchy",
Expand Down
47 changes: 46 additions & 1 deletion product_portfolio/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from dje.filters import DataspacedFilterSet
from dje.filters import DefaultOrderingFilter
from dje.filters import HasRelationFilter
from dje.filters import HasValueFilter
from dje.filters import MatchOrderedSearchFilter
from dje.filters import SearchFilter
from dje.widgets import BootstrapSelectMultipleWidget
Expand All @@ -34,8 +35,8 @@
from product_portfolio.models import ProductDependency
from product_portfolio.models import ProductPackage
from product_portfolio.models import ProductStatus
from vulnerabilities.filters import RISK_SCORE_RANGES
from vulnerabilities.filters import ScoreRangeFilter
from vulnerabilities.models import RISK_SCORE_RANGES
from vulnerabilities.models import Vulnerability
from vulnerabilities.models import VulnerabilityAnalysisMixin

Expand Down Expand Up @@ -129,6 +130,26 @@ class Meta:
]


class HasComplianceIssueFilter(django_filters.BooleanFilter):
"""Filter objects that have a compliance alert (warning or error) on their usage policy."""

def __init__(self, *args, **kwargs):
kwargs.setdefault("label", _("Compliance issues"))
kwargs.setdefault("field_name", "compliance_alert")
super().__init__(*args, **kwargs)

def filter(self, qs, value):
if value is None:
return qs
lookup = {f"{self.field_name}__in": ["warning", "error"]}
if value:
qs = qs.filter(**lookup)
else:
qs = qs.exclude(**lookup)

return qs.distinct() if self.distinct else qs


class BaseProductRelationFilterSet(DataspacedFilterSet):
field_name_prefix = None
dropdown_fields = [
Expand All @@ -148,6 +169,7 @@ class BaseProductRelationFilterSet(DataspacedFilterSet):
)
is_modified = BooleanChoiceFilter()
object_type = django_filters.CharFilter(
label=_("Item type"),
method="filter_object_type",
widget=DropDownWidget(
anchor="#inventory",
Expand All @@ -171,6 +193,21 @@ class BaseProductRelationFilterSet(DataspacedFilterSet):
label=_("Risk score"),
score_ranges=RISK_SCORE_RANGES,
)
licenses = django_filters.ModelMultipleChoiceFilter(
label=_("License"),
field_name="licenses__key",
to_field_name="key",
queryset=License.objects.only("key", "short_name", "dataspace__id"),
)
has_licenses = HasValueFilter(
label=_("Has licenses"),
field_name="license_expression",
)
license_compliance_issues = HasComplianceIssueFilter(
label=_("License compliance issues"),
field_name="licenses__usage_policy__compliance_alert",
distinct=True,
)

@staticmethod
def filter_object_type(queryset, name, value):
Expand Down Expand Up @@ -237,6 +274,10 @@ class ProductComponentFilterSet(BaseProductRelationFilterSet):
anchor="#inventory", right_align=True, link_content='<i class="fas fa-bug"></i>'
),
)
compliance_issues = HasComplianceIssueFilter(
field_name="component__usage_policy__compliance_alert",
distinct=True,
)

class Meta:
model = ProductComponent
Expand Down Expand Up @@ -307,6 +348,10 @@ class ProductPackageFilterSet(BaseProductRelationFilterSet):
("unknown", _("Reachability not known")),
),
)
compliance_issues = HasComplianceIssueFilter(
field_name="package__usage_policy__compliance_alert",
distinct=True,
)

class Meta:
model = ProductPackage
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
{% load i18n %}
<div class="row g-4 mb-4">
{# License panel #}
<div class="col-lg-6">
<div class="border rounded-3 p-3 h-100">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="fs-6 fw-medium mb-0">{% trans "License compliance" %}</h3>
{% if license_issues_count == 0 %}
<span class="badge text-bg-success">
{% trans "OK" %}
</span>
{% elif license_error_count > 0 %}
<span class="badge text-bg-danger">
{% trans "Error" %}
</span>
{% else %}
<span class="badge text-bg-warning">
{% trans "Warning" %}
</span>
{% endif %}
</div>

<p class="text-secondary small mb-2">
{% trans "License distribution" %} (top {{ license_distribution_limit }})
</p>
<table class="table table-sm mb-0">
<thead>
<tr>
<th class="fw-medium">{% trans "License" %}</th>
<th class="fw-medium text-end">{% trans "Packages" %}</th>
<th class="fw-medium text-end">{% trans "Policy" %}</th>
</tr>
</thead>
<tbody>
{% for license in license_distribution %}
<tr>
<td>
<a href="{% url 'license_library:license_details' product.dataspace.name license.key %}">
{{ license.spdx_license_key }}
</a>
</td>
<td class="text-end">
<a href="{{ product.get_absolute_url }}?inventory-licenses={{ license.key }}#inventory">
{{ license.package_count }}
</a>
</td>
<td class="text-end">
{% if license.compliance_alert == "error" %}
<span class="badge text-bg-danger">{% trans "Error" %}</span>
{% elif license.compliance_alert == "warning" %}
<span class="badge text-bg-warning">{% trans "Warning" %}</span>
{% else %}
<span class="badge text-bg-success">{% trans "OK" %}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if remaining_license_count > 0 %}
<div class=" mt-2 pt-2">
<small class="text-body-tertiary">
+ {{ remaining_license_count }} {% trans "other license" %}{{ remaining_license_count|pluralize }}{% if license_issues_count == 0 %}, {% trans "all within policy" %}{% endif %}
</small>
</div>
{% endif %}
</div>
</div>

{# Security panel #}
<div class="col-lg-6">
<div class="border rounded-3 p-3 h-100">
{# Header #}
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="fs-6 fw-medium mb-0">{% trans "Security compliance" %}</h3>
{% if vulnerability_count == 0 or above_threshold_count == 0 %}
<span class="badge text-bg-success">{% trans "OK" %}</span>
{% elif max_vulnerability_severity == "critical" %}
<span class="badge text-bg-danger">{% trans "Critical" %}</span>
{% elif max_vulnerability_severity == "high" %}
<span class="badge bg-warning-orange">{% trans "High" %}</span>
{% elif max_vulnerability_severity == "medium" %}
<span class="badge text-bg-warning">{% trans "Medium" %}</span>
{% else %}
<span class="badge text-bg-info">{% trans "Low" %}</span>
{% endif %}
</div>

{# Summary #}
{% if vulnerability_count > 0 %}
<p class="text-body-secondary small mb-3">
{% if risk_threshold_number %}
{% if above_threshold_count > 0 %}
{{ above_threshold_count }} {% trans "of" %} {{ vulnerability_count }}
{% trans "vulnerabilit" %}{{ vulnerability_count|pluralize:"y,ies" }}
{% trans "above risk threshold of" %} {{ risk_threshold_number }}
{% else %}
{{ vulnerability_count }}
{% trans "vulnerabilit" %}{{ vulnerability_count|pluralize:"y,ies" }},
{% trans "all below risk threshold of" %} {{ risk_threshold_number }}
{% endif %}
{% else %}
{{ vulnerability_count }}
{% trans "vulnerabilit" %}{{ vulnerability_count|pluralize:"y,ies" }}
— {% trans "no risk threshold set" %}
{% endif %}
</p>
{% endif %}

{# Vulnerability list #}
{% for vulnerability in vulnerabilities %}
<div class="d-flex align-items-center border-bottom gap-3 py-2">
<span data-bs-toggle="tooltip" title="{{ vulnerability.risk_score }}">
{% if vulnerability.risk_level == "critical" %}
<span class="badge text-bg-danger">{% trans "Critical" %}</span>
{% elif vulnerability.risk_level == "high" %}
<span class="badge bg-warning-orange">{% trans "High" %}</span>
{% elif vulnerability.risk_level == "medium" %}
<span class="badge text-bg-warning">{% trans "Medium" %}</span>
{% elif vulnerability.risk_level == "low" %}
<span class="badge text-bg-info">{% trans "Low" %}</span>
{% else %}
<span class="badge text-bg-secondary">{% trans "Unknown" %}</span>
{% endif %}
</span>
<a href="{{ vulnerability.resource_url }}" target="_blank" class="font-monospace small text-decoration-none" style="min-width: fit-content;">
{{ vulnerability.vulnerability_id }}
</a>
<span class="flex-grow-1 d-none d-md-inline small">{{ vulnerability.summary|truncatechars:70 }}</span>
</div>
{% empty %}
<div class="text-center text-body-tertiary py-4">
<i class="fas fa-shield-check fa-2x mb-2 d-block"></i>
{% trans "No known vulnerabilities" %}
</div>
{% endfor %}

{# View all link #}
{% if vulnerabilities|length < vulnerability_count %}
<div class="mt-2">
<a href="#vulnerabilities" class="text-decoration-none small" onclick="new bootstrap.Tab(document.querySelector('#tab_vulnerabilities-tab')).show()">
{% trans "View all" %} {{ vulnerability_count }} {% trans "vulnerabilities" %}
<i class="fas fa-arrow-right ms-1"></i>
</a>
</div>
{% endif %}
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{% load i18n humanize %}
<div class="row g-3 mb-4">
<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 packages" %}</div>
<div class="fs-4 fw-medium lh-sm">{{ total_packages|intcomma }}</div>
<div class="text-body-tertiary fs-xs mt-1">
{% if package_issues_count == 0 %}
{% trans "No policy violations" %}
{% else %}
<a href="?inventory-compliance_issues=true#inventory">
{{ package_issues_count }} {% trans "package policy violation" %}{{ package_issues_count|pluralize }}
</a>
{% 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 compliance" %}</div>
<div class="fs-4 fw-medium lh-sm {% if license_compliance_pct == 100 %}text-success{% elif license_compliance_pct >= 90 %}text-warning-orange{% else %}text-danger{% endif %}">
{{ license_compliance_pct }}%
</div>
<div class="text-body-tertiary fs-xs mt-1">
{% if packages_with_license_issues == 0 %}
{% trans "All packages within policy" %}
{% else %}
<a href="?inventory-license_compliance_issues=true#inventory">
{{ packages_with_license_issues }} {% trans "packages with license policy violations" %}
</a>
{% 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 coverage" %}</div>
<div class="fs-4 fw-medium lh-sm {% if license_coverage_pct == 100 %}text-success{% elif license_coverage_pct >= 90 %}text-warning-orange{% else %}text-danger{% endif %}">
{{ license_coverage_pct }}%
</div>
<div class="text-body-tertiary fs-xs mt-1">
{% if license_coverage_pct == 100 %}
{% trans "All packages have a license" %}
{% else %}
<a href="?inventory-has_licenses=no#inventory">
{{ package_without_license_count }} {% trans "packages without license" %}
</a>
{% 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 "Vulnerabilities" %}</div>
{% if vulnerability_count == 0 %}
<div class="fs-4 fw-medium lh-sm text-success">0</div>
<div class="text-body-tertiary fs-xs mt-1">{% trans "No known vulnerabilities" %}</div>
{% elif risk_threshold_number and above_threshold_count == 0 %}
<div class="fs-4 fw-medium lh-sm text-success">0</div>
<div class="text-body-tertiary fs-xs mt-1">
{{ vulnerability_count }} {% trans "below risk threshold of" %} {{ risk_threshold_number }}
</div>
{% elif risk_threshold_number %}
<div class="fs-4 fw-medium lh-sm text-danger">{{ above_threshold_count }}</div>
<div class="text-body-tertiary fs-xs mt-1">
{% trans "of" %} {{ vulnerability_count }} {% trans "above threshold of" %} {{ risk_threshold_number }}
</div>
{% else %}
<div class="fs-4 fw-medium lh-sm {% if max_vulnerability_severity == 'critical' %}text-danger{% elif max_vulnerability_severity == 'high' %}text-warning-orange{% elif max_vulnerability_severity == 'medium' %}text-warning{% else %}text-info{% endif %}">
{{ vulnerability_count }}
</div>
<div class="text-body-tertiary fs-xs mt-1">
{% if critical_count %}{{ critical_count }} {% trans "critical" %}{% endif %}{% if critical_count and high_count %}, {% endif %}{% if high_count %}{{ high_count }} {% trans "high" %}{% endif %}{% if medium_count and critical_count or medium_count and high_count %}, {% endif %}{% if medium_count %}{{ medium_count }} {% trans "medium" %}{% endif %}{% if low_count and vulnerability_count == low_count %}{{ low_count }} {% trans "low" %}{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% spaceless %}
{% include 'product_portfolio/compliance/metric_cards.html' %}
{% include 'product_portfolio/compliance/compliance_panels.html' %}
{% endspaceless %}
Loading
Loading