From 34f3905a720d20f44487aa6def32a6e99bba2cb5 Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 24 Oct 2024 13:04:09 +0400 Subject: [PATCH 1/2] 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 2/2] 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):