From 34f3905a720d20f44487aa6def32a6e99bba2cb5 Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 24 Oct 2024 13:04:09 +0400 Subject: [PATCH 01/33] Add ProductVulnerabilityAnalysis model implementation #98 Signed-off-by: tdruez --- dejacode/static/css/dejacode_bootstrap.css | 7 +++- .../0008_productvulnerabilityanalysis.py | 42 +++++++++++++++++++ product_portfolio/models.py | 25 +++++++++++ vulnerabilities/models.py | 2 +- 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 product_portfolio/migrations/0008_productvulnerabilityanalysis.py diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index 957dcdd4..d25cfb66 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -386,8 +386,11 @@ table.vulnerabilities-table .column-summary { #tab_vulnerabilities .column-max_score { width: 105px; } -#tab_vulnerabilities .column-column-affected_packages { - width: 320px; +#tab_vulnerabilities .column-affected_packages { + min-width: 200px; +} +#tab_vulnerabilities .column-summary { + width: 300px; } /* -- Dependency tab -- */ diff --git a/product_portfolio/migrations/0008_productvulnerabilityanalysis.py b/product_portfolio/migrations/0008_productvulnerabilityanalysis.py new file mode 100644 index 00000000..6dacbe6c --- /dev/null +++ b/product_portfolio/migrations/0008_productvulnerabilityanalysis.py @@ -0,0 +1,42 @@ +# Generated by Django 5.0.9 on 2024-10-24 09:03 + +import django.contrib.postgres.fields +import django.db.models.deletion +import dje.fields +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dje', '0004_dataspace_vulnerabilities_updated_at'), + ('product_portfolio', '0007_alter_scancodeproject_type'), + ('vulnerabilities', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ProductVulnerabilityAnalysis', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')), + ('state', models.CharField(blank=True, choices=[('resolved', 'Resolved'), ('resolved_with_pedigree', 'Resolved With Pedigree'), ('exploitable', 'Exploitable'), ('in_triage', 'In Triage'), ('false_positive', 'False Positive'), ('not_affected', 'Not Affected')], help_text='Declares the current state of an occurrence of a vulnerability, after automated or manual analysis.', max_length=25)), + ('justification', models.CharField(blank=True, choices=[('code_not_present', 'Code Not Present'), ('code_not_reachable', 'Code Not Reachable'), ('protected_at_perimeter', 'Protected At Perimeter'), ('protected_at_runtime', 'Protected At Runtime'), ('protected_by_compiler', 'Protected By Compiler'), ('protected_by_mitigating_control', 'Protected By Mitigating Control'), ('requires_configuration', 'Requires Configuration'), ('requires_dependency', 'Requires Dependency'), ('requires_environment', 'Requires Environment')], help_text='The rationale of why the impact analysis state was asserted.', max_length=35)), + ('responses', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('can_not_fix', 'Can Not Fix'), ('rollback', 'Rollback'), ('update', 'Update'), ('will_not_fix', 'Will Not Fix'), ('workaround_available', 'Workaround Available')], max_length=20), blank=True, help_text='A response to the vulnerability by the manufacturer, supplier, or project responsible for the affected component or service. More than one response is allowed. Responses are strongly encouraged for vulnerabilities where the analysis state is exploitable.', null=True, size=None)), + ('detail', models.TextField(blank=True, help_text='Detailed description of the impact including methods used during assessment. If a vulnerability is not exploitable, this field should include specific details on why the component or service is not impacted by this vulnerability.')), + ('first_issued', models.DateTimeField(auto_now_add=True, help_text='The date and time (timestamp) when the analysis was first issued.')), + ('last_updated', models.DateTimeField(auto_now=True, help_text='The date and time (timestamp) when the analysis was last updated.')), + ('created_by', models.ForeignKey(editable=False, help_text='The application user who created the object.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='created_%(class)ss', serialize=False, to=settings.AUTH_USER_MODEL)), + ('dataspace', models.ForeignKey(editable=False, help_text='A Dataspace is an independent, exclusive set of DejaCode data, which can be either nexB master reference data or installation-specific data.', on_delete=django.db.models.deletion.PROTECT, to='dje.dataspace')), + ('last_modified_by', dje.fields.LastModifiedByField(editable=False, help_text='The application user who last modified the object.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='modified_%(class)ss', serialize=False, to=settings.AUTH_USER_MODEL)), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vulnerability_analyses', to='product_portfolio.product')), + ('vulnerability', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_vulnerability_analyses', to='vulnerabilities.vulnerability')), + ], + options={ + 'unique_together': {('dataspace', 'uuid'), ('product', 'vulnerability')}, + }, + ), + ] diff --git a/product_portfolio/models.py b/product_portfolio/models.py index d4355f16..c3145022 100644 --- a/product_portfolio/models.py +++ b/product_portfolio/models.py @@ -37,12 +37,14 @@ from dje.models import History from dje.models import HistoryFieldsMixin from dje.models import ReferenceNotesMixin +from dje.models import HistoryUserFieldsMixin from dje.models import colored_icon_mixin_factory from dje.validators import generic_uri_validator from dje.validators import validate_url_segment from dje.validators import validate_version from vulnerabilities.fetch import fetch_for_queryset from vulnerabilities.models import Vulnerability +from vulnerabilities.models import VulnerabilityAnalysisMixin RELATION_LICENSE_EXPRESSION_HELP_TEXT = _( "The License Expression assigned to a DejaCode Product Package or Product " @@ -1466,3 +1468,26 @@ def save(self, *args, **kwargs): "The 'for_package' cannot be the same as 'resolved_to_package'." ) super().save(*args, **kwargs) + + +class ProductVulnerabilityAnalysis( + VulnerabilityAnalysisMixin, + HistoryUserFieldsMixin, + DataspacedModel, +): + product = models.ForeignKey( + to="product_portfolio.Product", + related_name="vulnerability_analyses", + on_delete=models.CASCADE, + ) + vulnerability = models.ForeignKey( + to="vulnerabilities.Vulnerability", + related_name="product_vulnerability_analyses", + on_delete=models.CASCADE, + ) + + class Meta: + unique_together = (("product", "vulnerability"), ("dataspace", "uuid")) + + def __str__(self): + return f"{self.vulnerability} analysis in {self.product}." diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index dbd789b4..0e5e7064 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -278,7 +278,7 @@ class State(models.TextChoices): class Justification(models.TextChoices): CODE_NOT_PRESENT = "code_not_present" CODE_NOT_REACHABLE = "code_not_reachable" - PROTECTED_AT_PERIMITER = "protected_at_perimeter" + PROTECTED_AT_PERIMETER = "protected_at_perimeter" PROTECTED_AT_RUNTIME = "protected_at_runtime" PROTECTED_BY_COMPILER = "protected_by_compiler" PROTECTED_BY_MITIGATING_CONTROL = "protected_by_mitigating_control" From af916f9fd8a454847ed24bcae047d5aba53cb12b Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 30 Oct 2024 18:55:00 +0400 Subject: [PATCH 02/33] Add a new Exploitability analysis column #98 Signed-off-by: tdruez --- .../product_portfolio/tabs/tab_vulnerabilities.html | 10 ++++++++++ product_portfolio/views.py | 1 + 2 files changed, 11 insertions(+) diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html index 1931d05d..b374fc1b 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html @@ -51,6 +51,16 @@ {% endfor %} + + {% if vulnerability.product_vulnerability_analyses.get %} +
    +
  • State: {{ vulnerability.product_vulnerability_analyses.get.state }}
  • +
  • Justification: {{ vulnerability.product_vulnerability_analyses.get.justification }}
  • +
  • Responses: {{ vulnerability.product_vulnerability_analyses.get.responses }}
  • +
  • Detail: {{ vulnerability.product_vulnerability_analyses.get.detail }}
  • +
+ {% endif %} + {% empty %} diff --git a/product_portfolio/views.py b/product_portfolio/views.py index df994e7c..274ea498 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -1100,6 +1100,7 @@ class ProductTabVulnerabilitiesView( Header("max_score", _("Score"), help_text="Severity score range", filter="max_score"), Header("summary", _("Summary")), Header("affected_packages", _("Affected packages"), help_text="Affected product packages"), + Header("exploitability", _("Exploitability analysis"), help_text="TODO"), ) def get_context_data(self, **kwargs): From 3c883d9b361f437e0678d54637dda915e50fe05c Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 31 Oct 2024 16:13:34 +0400 Subject: [PATCH 03/33] Merge main and remake migration #98 Signed-off-by: tdruez --- ...bilityanalysis.py => 0009_productvulnerabilityanalysis.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename product_portfolio/migrations/{0008_productvulnerabilityanalysis.py => 0009_productvulnerabilityanalysis.py} (97%) diff --git a/product_portfolio/migrations/0008_productvulnerabilityanalysis.py b/product_portfolio/migrations/0009_productvulnerabilityanalysis.py similarity index 97% rename from product_portfolio/migrations/0008_productvulnerabilityanalysis.py rename to product_portfolio/migrations/0009_productvulnerabilityanalysis.py index 6dacbe6c..cbb99f50 100644 --- a/product_portfolio/migrations/0008_productvulnerabilityanalysis.py +++ b/product_portfolio/migrations/0009_productvulnerabilityanalysis.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.9 on 2024-10-24 09:03 +# Generated by Django 5.0.9 on 2024-10-31 11:53 import django.contrib.postgres.fields import django.db.models.deletion @@ -12,7 +12,7 @@ class Migration(migrations.Migration): dependencies = [ ('dje', '0004_dataspace_vulnerabilities_updated_at'), - ('product_portfolio', '0007_alter_scancodeproject_type'), + ('product_portfolio', '0008_productdependency_is_resolved_to_is_pinned'), ('vulnerabilities', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] From 02e67c552ff6d3a055284c17dbf88f775fed68fa Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 1 Nov 2024 16:17:29 +0400 Subject: [PATCH 04/33] Move the aliases in the first column #98 Signed-off-by: tdruez --- .../product_portfolio/tabs/tab_vulnerabilities.html | 6 +++--- product_portfolio/views.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html index b374fc1b..61f4bb72 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html @@ -16,9 +16,9 @@ {{ vulnerability.vulnerability_id }} {% endif %} - - - {% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %} +
+ {% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %} +
{% if vulnerability.min_score %} diff --git a/product_portfolio/views.py b/product_portfolio/views.py index c956c541..54bd6a30 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -1096,7 +1096,6 @@ class ProductTabVulnerabilitiesView( filterset_class = VulnerabilityFilterSet table_headers = ( Header("vulnerability_id", _("Vulnerability")), - Header("aliases", _("Aliases")), Header("max_score", _("Score"), help_text="Severity score range", filter="max_score"), Header("summary", _("Summary")), Header("affected_packages", _("Affected packages"), help_text="Affected product packages"), From c14c4703f42f43e5cf3ed7866d7d65a27bf9fda0 Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 6 Nov 2024 19:03:50 +0400 Subject: [PATCH 05/33] Add prototype to add/edit the Vulnerability analysis #98 Signed-off-by: tdruez --- dejacode/static/css/dejacode_bootstrap.css | 4 ++ product_portfolio/forms.py | 50 +++++++++++++++++ product_portfolio/models.py | 2 +- .../modals/vulnerability_analysis_modal.html | 19 +++++++ .../product_portfolio/product_details.html | 56 +++++++++++++++++++ .../tabs/tab_vulnerabilities.html | 21 +++++-- product_portfolio/urls.py | 2 + product_portfolio/views.py | 30 ++++++++++ 8 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 product_portfolio/templates/product_portfolio/modals/vulnerability_analysis_modal.html diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index c661ba4a..2dbc87db 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -531,6 +531,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/product_portfolio/forms.py b/product_portfolio/forms.py index cc356f97..91f9f019 100644 --- a/product_portfolio/forms.py +++ b/product_portfolio/forms.py @@ -52,6 +52,7 @@ from product_portfolio.models import Product from product_portfolio.models import ProductComponent from product_portfolio.models import ProductPackage +from product_portfolio.models import ProductVulnerabilityAnalysis from product_portfolio.models import ScanCodeProject @@ -945,3 +946,52 @@ def submit(self, product, user): scancodeproject_uuid=scancode_project.uuid, ) ) + + +class ProductVulnerabilityAnalysisForm(DataspacedModelForm): + # TODO: Replace this by proper model field + responses = forms.MultipleChoiceField( + choices=ProductVulnerabilityAnalysis.Response.choices, + widget=forms.CheckboxSelectMultiple, + required=False, + help_text=_( + "A response to the vulnerability by the manufacturer, supplier, or project " + "responsible for the affected component or service. " + "More than one response is allowed. " + "Responses are strongly encouraged for vulnerabilities where the analysis " + "state is exploitable." + ), + ) + + class Meta: + model = ProductVulnerabilityAnalysis + fields = [ + "product", + "vulnerability", + "state", + "justification", + "responses", + "detail", + ] + widgets = { + "product": forms.widgets.HiddenInput, + "vulnerability": forms.widgets.HiddenInput, + "detail": forms.Textarea(attrs={"rows": 3}), + } + + def __init__(self, user, *args, **kwargs): + super().__init__(user, *args, **kwargs) + + product_field = self.fields["product"] + perms = ["view_product", "change_product"] + product_field.queryset = Product.objects.get_queryset(user, perms=perms) + + @property + def helper(self): + helper = FormHelper() + helper.form_method = "post" + helper.form_id = "product-vulnerability-analysis-form" + helper.form_tag = False + helper.modal_title = "Vulnerability analysis" + helper.modal_id = "vulnerability-analysis-modal" + return helper diff --git a/product_portfolio/models.py b/product_portfolio/models.py index 7fbb28a9..b15b7bb9 100644 --- a/product_portfolio/models.py +++ b/product_portfolio/models.py @@ -36,8 +36,8 @@ from dje.models import DataspacedQuerySet from dje.models import History from dje.models import HistoryFieldsMixin -from dje.models import ReferenceNotesMixin from dje.models import HistoryUserFieldsMixin +from dje.models import ReferenceNotesMixin from dje.models import colored_icon_mixin_factory from dje.validators import generic_uri_validator from dje.validators import validate_url_segment diff --git a/product_portfolio/templates/product_portfolio/modals/vulnerability_analysis_modal.html b/product_portfolio/templates/product_portfolio/modals/vulnerability_analysis_modal.html new file mode 100644 index 00000000..1bf3a48c --- /dev/null +++ b/product_portfolio/templates/product_portfolio/modals/vulnerability_analysis_modal.html @@ -0,0 +1,19 @@ +{% load crispy_forms_tags %} + \ 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 047b8695..caa664a5 100644 --- a/product_portfolio/templates/product_portfolio/product_details.html +++ b/product_portfolio/templates/product_portfolio/product_details.html @@ -119,6 +119,7 @@ {% if tabsets.Imports %} {% include 'product_portfolio/includes/scancode_project_status_modal.html' %} {% endif %} + {% include 'product_portfolio/modals/vulnerability_analysis_modal.html' %} {% endblock %} {% block extrastyle %} @@ -232,6 +233,61 @@ {% endif %} + {# TODO: Replace by htmx %] + + {% if purldb_enabled %} {% endif %} - {# TODO: Replace by htmx %] + {# TODO: Replace by htmx #} {% endif %} - {# TODO: Replace by htmx #} - + }); + + {% endif %} {% if purldb_enabled %} {% endif %} - {% if request.user.dataspace.enable_vulnerablecodedb_access %} + {% if request.user.dataspace.enable_vulnerablecodedb_access and not tabsets.Vulnerabilities.disabled %} {% endif %} - {% if request.user.dataspace.enable_vulnerablecodedb_access and not tabsets.Vulnerabilities.disabled %} + {% if request.user.dataspace.enable_vulnerablecodedb_access and product.vulnerability_count %}