From 8824bc19fcebaba700d55325512eda9f9e68aab8 Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 23 Mar 2026 09:57:52 +0400 Subject: [PATCH 01/29] add licenses filter on BaseProductRelationFilterSet Signed-off-by: tdruez --- product_portfolio/filters.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/product_portfolio/filters.py b/product_portfolio/filters.py index 8b13f182..acc312b5 100644 --- a/product_portfolio/filters.py +++ b/product_portfolio/filters.py @@ -171,6 +171,12 @@ 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"), + ) @staticmethod def filter_object_type(queryset, name, value): From c2055a7ede8f678cb446a5a7789a431b3664addb Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 23 Mar 2026 13:53:13 +0400 Subject: [PATCH 02/29] add a get_risk_score_label function Signed-off-by: tdruez --- component_catalog/api.py | 2 +- product_portfolio/filters.py | 2 +- vulnerabilities/api.py | 2 +- vulnerabilities/filters.py | 8 +------- vulnerabilities/models.py | 23 +++++++++++++++++++++++ 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/component_catalog/api.py b/component_catalog/api.py index e00c7a5b..abd8705f 100644 --- a/component_catalog/api.py +++ b/component_catalog/api.py @@ -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: diff --git a/product_portfolio/filters.py b/product_portfolio/filters.py index acc312b5..c9a0108c 100644 --- a/product_portfolio/filters.py +++ b/product_portfolio/filters.py @@ -34,8 +34,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 diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index 090a2f00..b1c62f9e 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -20,8 +20,8 @@ from dje.api_custom import TabPermission from dje.filters import LastModifiedDateFilter from dje.filters import MultipleUUIDFilter -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 VulnerabilityAnalysis diff --git a/vulnerabilities/filters.py b/vulnerabilities/filters.py index dd9dcc31..e63a20a2 100644 --- a/vulnerabilities/filters.py +++ b/vulnerabilities/filters.py @@ -16,16 +16,10 @@ from dje.filters import SearchFilter from dje.widgets import DropDownRightWidget from dje.widgets import SortDropDownWidget +from vulnerabilities.models import RISK_SCORE_RANGES from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityAnalysisMixin -RISK_SCORE_RANGES = { - "low": (0.1, 2.9), - "medium": (3.0, 5.9), - "high": (6.0, 7.9), - "critical": (8.0, 10.0), -} - class NullsLastOrderingFilter(django_filters.OrderingFilter): """ diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 4baeefc8..84fb3bc4 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -27,6 +27,24 @@ logger = logging.getLogger("dje") +RISK_SCORE_RANGES = { + "low": (0.1, 2.9), + "medium": (3.0, 5.9), + "high": (6.0, 7.9), + "critical": (8.0, 10.0), +} + + +def get_risk_score_label(score): + """Return the severity label for a given risk score.""" + if score is None: + return "" + score = float(score) + for label, (low, high) in RISK_SCORE_RANGES.items(): + if low <= score <= high: + return label + return "" + class VulnerabilityQuerySet(DataspacedQuerySet): def with_affected_products_count(self): @@ -171,6 +189,11 @@ def cve(self): if alias.startswith("CVE-"): return alias + @property + def risk_score_label(self): + """Return the severity label for this risk score.""" + return get_risk_score_label(self.risk_score) + def add_affected(self, instances): """Assign the ``instances`` (Package or Product) as affected by this vulnerability.""" if not isinstance(instances, (list, tuple, models.QuerySet)): From 333981a740a0be0948efa9af4206b058c7b5d35e Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 23 Mar 2026 14:43:21 +0400 Subject: [PATCH 03/29] prototype of the product compliance dashboard Signed-off-by: tdruez --- dejacode/static/css/dejacode_bootstrap.css | 3 + product_portfolio/models.py | 3 + .../compliance/compliance_panels.html | 145 ++++++++++++++++++ .../compliance/metric_cards.html | 66 ++++++++ .../product_portfolio/product_compliance.html | 24 +++ .../product_portfolio/product_details.html | 1 + product_portfolio/urls.py | 2 + product_portfolio/views.py | 85 ++++++++++ 8 files changed, 329 insertions(+) create mode 100644 product_portfolio/templates/product_portfolio/compliance/compliance_panels.html create mode 100644 product_portfolio/templates/product_portfolio/compliance/metric_cards.html create mode 100644 product_portfolio/templates/product_portfolio/product_compliance.html diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index d71dfd80..d888e043 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -43,6 +43,9 @@ a.dropdown-item:hover { .fs-110pct { font-size: 110%; } +.fs-xs { + font-size: .75rem; +} .header { margin-bottom: 1rem; } diff --git a/product_portfolio/models.py b/product_portfolio/models.py index 9cb6bdca..a61540cc 100644 --- a/product_portfolio/models.py +++ b/product_portfolio/models.py @@ -333,6 +333,9 @@ def get_manage_packages_url(self): def get_license_summary_url(self): return self.get_url("license_summary") + def get_compliance_detail_url(self): + return self.get_url("compliance_detail") + def get_check_package_version_url(self): return self.get_url("check_package_version") diff --git a/product_portfolio/templates/product_portfolio/compliance/compliance_panels.html b/product_portfolio/templates/product_portfolio/compliance/compliance_panels.html new file mode 100644 index 00000000..cdfa9af5 --- /dev/null +++ b/product_portfolio/templates/product_portfolio/compliance/compliance_panels.html @@ -0,0 +1,145 @@ +{% load i18n %} +
+ {# License panel #} +
+
+
+

{% trans "License compliance" %}

+ {% if license_issues_count == 0 %} + + {% trans "OK" %} + + {% elif license_error_count > 0 %} + + {{ license_error_count }} {% trans "error" %}{{ license_error_count|pluralize }} + + {% else %} + + {{ license_warning_count }} {% trans "warning" %}{{ license_warning_count|pluralize }} + + {% endif %} +
+ +

+ {% trans "License distribution" %} (top {{ license_distribution_limit }}) +

+ + + + + + + + + + {% for license in license_distribution %} + + + + + + {% endfor %} + +
{% trans "License" %}{% trans "Packages" %}{% trans "Policy" %}
+ + {{ license.spdx_license_key }} + + + + {{ license.package_count }} + + + {% if license.usage_policy__compliance_alert == "error" %} + {% trans "Error" %} + {% elif license.usage_policy__compliance_alert == "warning" %} + {% trans "Warning" %} + {% else %} + {% trans "OK" %} + {% endif %} +
+ {% if remaining_license_count > 0 %} +
+ + + {{ remaining_license_count }} {% trans "other license" %}{{ remaining_license_count|pluralize }}{% if license_issues_count == 0 %}, {% trans "all within policy" %}{% endif %} + +
+ {% endif %} +
+
+ + {# Security panel #} +
+
+
+

{% trans "Security compliance" %}

+ {% if vulnerability_count == 0 %} + {% trans "OK" %} + {% elif above_threshold_count == 0 %} + {% trans "OK" %} + {% elif max_vulnerability_severity == "critical" %} + {% trans "Critical" %} + {% elif max_vulnerability_severity == "high" %} + {% trans "High" %} + {% else %} + {% trans "Medium" %} + {% endif %} +
+ + {% if vulnerability_count > 0 and above_threshold_count == 0 %} +

+ {{ vulnerability_count }} {% trans "vulnerabilit" %}{{ vulnerability_count|pluralize:"y,ies" }} + {% trans "below risk threshold" %} ({{ risk_threshold }}) +

+ {% elif above_threshold_count > 0 %} +

+ {{ above_threshold_count }} {% trans "vulnerabilit" %}{{ above_threshold_count|pluralize:"y,ies" }} + {% trans "at or above risk threshold" %} ({{ risk_threshold }}) +

+ {% endif %} + + {% for vulnerability in vulnerabilities %} +
+ {% if vulnerability.risk_score_label == "critical" %} + {% trans "Critical" %} + {% elif vulnerability.risk_score_label == "high" %} + {% trans "High" %} + {% elif vulnerability.risk_score_label == "medium" %} + {% trans "Medium" %} + {% elif vulnerability.risk_score_label == "low" %} + {% trans "Low" %} + {% else %} + {% trans "Unknown" %} + {% endif %} + + {{ vulnerability.vulnerability_id }} + + {{ vulnerability.summary|truncatewords:10 }} + {{ vulnerability.package }} +
+ {% empty %} +
+ + {% trans "No known vulnerabilities" %} +
+ {% endfor %} + + {% if vulnerabilities|length < vulnerability_count %} + + {% endif %} + + {% if risk_threshold and above_threshold_count == 0 and vulnerability_count > 0 %} +
+ + {% trans "Risk threshold set to" %} {{ risk_threshold }} — + {% trans "items shown are informational only" %} + +
+ {% endif %} +
+
+
\ No newline at end of file diff --git a/product_portfolio/templates/product_portfolio/compliance/metric_cards.html b/product_portfolio/templates/product_portfolio/compliance/metric_cards.html new file mode 100644 index 00000000..40a8a59b --- /dev/null +++ b/product_portfolio/templates/product_portfolio/compliance/metric_cards.html @@ -0,0 +1,66 @@ +{% load i18n humanize %} +
+
+
+
{% trans "Total packages" %}
+
{{ total_packages|intcomma }}
+
+ TODO: {% trans "policy violation" %} +
+
+
+
+
+
{% trans "License compliance" %}
+
+ {{ license_compliance_pct }}% +
+
+ {% if license_issues_count == 0 %} + {% trans "No policy violations" %} + {% else %} + {{ license_issues_count }} {% trans "policy violation" %}{{ license_issues_count|pluralize }} + {% endif %} +
+
+
+
+
+
{% trans "Vulnerabilities" %}
+
+ {{ vulnerability_count }} +
+
+ {% if vulnerability_count == 0 %} + {% trans "No known vulnerabilities" %} + {% elif max_vulnerability_severity == "critical" %} + {{ critical_count }} {% trans "critical" %}{% if high_count %}, {{ high_count }} {% trans "high" %}{% endif %} + {% elif max_vulnerability_severity == "high" %} + {{ high_count }} {% trans "high" %}{% if medium_count %}, {{ medium_count }} {% trans "medium" %}{% endif %} + {% else %} + {% trans "All below risk threshold" %} + {% endif %} +
+
+
+
+
+
{% trans "Package coverage" %}
+
+ {{ no_license_count }} +
+
+ {% if no_license_count %} + {% trans "package" %}{{ no_license_count|pluralize }} {% trans "with no license" %} + {% else %} + {% trans "All packages have a license" %} + {% endif %} +
+ {% if no_policy_count %} +
+ {{ no_policy_count }} {% trans "without usage policy" %} +
+ {% endif %} +
+
+
\ No newline at end of file diff --git a/product_portfolio/templates/product_portfolio/product_compliance.html b/product_portfolio/templates/product_portfolio/product_compliance.html new file mode 100644 index 00000000..8c61fac9 --- /dev/null +++ b/product_portfolio/templates/product_portfolio/product_compliance.html @@ -0,0 +1,24 @@ +{% extends "bootstrap_base.html" %} +{% load i18n %} + +{% block content %} + + +
+

{{ product.name }}

+ {% if overall_status == "ok" %} + {% trans "Compliant" %} + {% elif overall_status == "warning" %} + {% trans "Warning" %} + {% else %} + {% trans "Non-compliant" %} + {% endif %} +
+ +{% include 'product_portfolio/compliance/metric_cards.html' %} +{% include 'product_portfolio/compliance/compliance_panels.html' %} +{% endblock %} \ No newline at end of file diff --git a/product_portfolio/templates/product_portfolio/product_details.html b/product_portfolio/templates/product_portfolio/product_details.html index 6f4f11cd..5fff9ab2 100644 --- a/product_portfolio/templates/product_portfolio/product_details.html +++ b/product_portfolio/templates/product_portfolio/product_details.html @@ -4,6 +4,7 @@ {% block pager-toolbar %} {% if is_user_dataspace %} + Compliance {% if product.is_locked %}