diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cb2d070a..665cf2ee 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,18 @@ Release notes vulnerability data is available. https://github.com/aboutcode-org/dejacode/issues/98 +- Introduce a new VulnerabilityAnalysis model based on CycloneDX spec: + https://cyclonedx.org/docs/1.6/json/#vulnerabilities_items_analysis + A VulnerabilityAnalysis is always assigned to a Vulnerability object and a + ProductPackage relation. + The values for a VulnerabilityAnalysis are display in the Product "Vulnerabilities" + tab. + A "Edit" button can be used to open a form in a model to provided analysis data. + Those new VEX related columns can be sorted and filtered. + The VulnerabilityAnalysis data is exported in the VEX (only) and SBOM+VEX (combined) + outputs. + https://github.com/aboutcode-org/dejacode/issues/98 + ### Version 5.2.1 - Fix the models documentation navigation. diff --git a/component_catalog/filters.py b/component_catalog/filters.py index 83294143..58425811 100644 --- a/component_catalog/filters.py +++ b/component_catalog/filters.py @@ -49,6 +49,10 @@ class ComponentFilterSet(DataspacedFilterSet): "primary_language", "usage_policy", ] + dropdown_fields = [ + "type", + "usage_policy", + ] q = MatchOrderedSearchFilter( label=_("Search"), match_order_fields=["name"], @@ -122,9 +126,7 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.filters["usage_policy"].extra["to_field_name"] = "label" - self.filters["usage_policy"].extra["widget"] = DropDownRightWidget() self.filters["type"].extra["to_field_name"] = "label" - self.filters["type"].extra["widget"] = DropDownRightWidget() @cached_property def sort_value(self): @@ -180,6 +182,7 @@ def filter(self, qs, value): class PackageFilterSet(DataspacedFilterSet): + dropdown_fields = ["usage_policy"] q = PackageSearchFilter( label=_("Search"), match_order_fields=[ @@ -262,7 +265,6 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.filters["usage_policy"].extra["to_field_name"] = "label" - self.filters["usage_policy"].extra["widget"] = DropDownRightWidget() @cached_property def sort_value(self): diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index 42d1c652..ac1656a6 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -92,6 +92,7 @@ table.text-break thead { .bg-warning-orange { background-color: var(--bs-orange); } + /* -- Dark there fixes -- */ [data-bs-theme=dark] .btn-outline-dark { --bs-btn-color: var(--bs-tertiary-color); @@ -390,17 +391,26 @@ table.vulnerabilities-table .column-summary { min-width: 300px; } #tab_vulnerabilities .column-exploitability { - width: 150px; + width: 155px; } #tab_vulnerabilities .column-weighted_severity { - width: 115px; + width: 120px; } #tab_vulnerabilities .column-risk_score { - width: 95px; + width: 90px; } #tab_vulnerabilities .column-summary { width: 300px; } +#tab_vulnerabilities .column-vulnerability_analyses__state { + min-width: 105px; +} +#tab_vulnerabilities .column-vulnerability_analyses__justification { + min-width: 130px; +} +#tab_vulnerabilities .column-vulnerability_analyses__responses { + min-width: 120px; +} /* -- Dependency tab -- */ #tab_dependencies .column-for_package { @@ -540,6 +550,10 @@ table.purldb-table .column-license_expression { vertical-align: middle; } +#vulnerability-analysis-form fieldset legend { + font-size: 1rem; +} + /* -- Object form (add/edit) -- */ .datepicker { width: 130px; diff --git a/dje/filters.py b/dje/filters.py index 0aab8a31..05bbd7a9 100644 --- a/dje/filters.py +++ b/dje/filters.py @@ -43,6 +43,7 @@ from dje.utils import extract_name_version from dje.utils import get_uuids_list_sorted from dje.utils import remove_field_from_query_dict +from dje.widgets import DropDownRightWidget IS_FILTER_LOOKUP_VAR = "_filter_lookup" @@ -80,6 +81,7 @@ def get_filters_breadcrumbs(self): class DataspacedFilterSet(FilterSetUtilsMixin, django_filters.FilterSet): related_only = [] + dropdown_fields = [] def __init__(self, *args, **kwargs): try: @@ -90,6 +92,7 @@ def __init__(self, *args, **kwargs): self.dynamic_qs = kwargs.pop("dynamic_qs", True) self.parent_qs_cache = {} self.anchor = kwargs.pop("anchor", None) + self.dropdown_fields = kwargs.pop("dropdown_fields", []) or self.dropdown_fields super().__init__(*args, **kwargs) @@ -106,6 +109,9 @@ def __init__(self, *args, **kwargs): model_name = self._meta.model._meta.model_name usage_policy.queryset = usage_policy.queryset.filter(content_type__model=model_name) + for field_name in self.dropdown_fields: + self.filters[field_name].extra["widget"] = DropDownRightWidget(anchor=self.anchor) + def apply_related_only(self, field_name, filter_): """ Limit the filter choices to the values used on the parent queryset. diff --git a/dje/outputs.py b/dje/outputs.py index 41be7b0a..29eefa98 100644 --- a/dje/outputs.py +++ b/dje/outputs.py @@ -132,10 +132,20 @@ def get_cyclonedx_bom(instance, user, include_components=True, include_vex=False if include_vex: vulnerability_qs = instance.get_vulnerability_qs(prefetch_related_packages=True) - vulnerabilities = [ - vulnerability.as_cyclonedx(affected_instances=vulnerability.affected_packages.all()) - for vulnerability in vulnerability_qs - ] + vulnerabilities = [] + for vulnerability in vulnerability_qs: + analysis = None + vulnerability_analyses = vulnerability.vulnerability_analyses.all() + if len(vulnerability_analyses) == 1: + analysis = vulnerability_analyses[0] + + vulnerabilities.append( + vulnerability.as_cyclonedx( + affected_instances=vulnerability.affected_packages.all(), + analysis=analysis, + ) + ) + bom.vulnerabilities = vulnerabilities return bom diff --git a/dje/templates/includes/object_list_table_header.html b/dje/templates/includes/object_list_table_header.html index f28dedf7..d6d4f5ea 100644 --- a/dje/templates/includes/object_list_table_header.html +++ b/dje/templates/includes/object_list_table_header.html @@ -36,5 +36,8 @@ {% endif %} {% endfor %} + {% if include_actions %} +