diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index b8bb703a6..c4a82b529 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -54,13 +54,25 @@ def to_representation(self, instance): class VulnerabilityReferenceSerializer(serializers.ModelSerializer): - scores = VulnerabilitySeveritySerializer(many=True, source="vulnerabilityseverity_set") + scores = serializers.SerializerMethodField() reference_url = serializers.CharField(source="url") class Meta: model = VulnerabilityReference fields = ["reference_url", "reference_id", "reference_type", "scores", "url"] + def get_scores(self, instance): + severities_related_to_reference = [ + severity + for severity in self.context.get("severities", []) + if severity.url == instance.url + ] + + return VulnerabilitySeveritySerializer( + severities_related_to_reference, + many=True, + ).data + class BaseResourceSerializer(serializers.HyperlinkedModelSerializer): """ @@ -199,8 +211,7 @@ class VulnerabilitySerializer(BaseResourceSerializer): many=True, source="filtered_fixed_packages", read_only=True ) affected_packages = MinimalPackageSerializer(many=True, read_only=True) - - references = VulnerabilityReferenceSerializer(many=True, source="vulnerabilityreference_set") + references = serializers.SerializerMethodField() aliases = AliasSerializer(many=True, source="alias") exploits = ExploitSerializer(many=True, read_only=True) weaknesses = WeaknessSerializer(many=True) @@ -214,10 +225,22 @@ def to_representation(self, instance): return data + def get_references(self, vulnerability): + references = vulnerability.vulnerabilityreference_set.all() + severities = vulnerability.severities.all() + + serialized_references = VulnerabilityReferenceSerializer( + references, + context={"severities": severities}, + many=True, + ).data + + return serialized_references + def get_severity_range_score(self, instance): severity_vectors = [] severity_values = set() - for s in instance.severities: + for s in instance.severities.all(): if s.scoring_system == EPSS.identifier: continue diff --git a/vulnerabilities/api_extension.py b/vulnerabilities/api_extension.py index 79cb2994b..7a13baf42 100644 --- a/vulnerabilities/api_extension.py +++ b/vulnerabilities/api_extension.py @@ -84,11 +84,10 @@ class Meta: class V2VulnerabilitySeveritySerializer(ModelSerializer): score = CharField(source="value") - reference = V2VulnerabilityReferenceSerializer() class Meta: model = VulnerabilitySeverity - fields = ("score", "scoring_system", "scoring_elements", "published_at", "reference") + fields = ("url", "score", "scoring_system", "scoring_elements", "published_at") class V2WeaknessSerializer(ModelSerializer): @@ -127,9 +126,9 @@ class V2VulnerabilitySerializer(ModelSerializer): aliases = SerializerMethodField("get_aliases") weaknesses = V2WeaknessSerializer(many=True, source="weaknesses_set") - scores = V2VulnerabilitySeveritySerializer(many=True, source="vulnerabilityseverity_set") references = V2VulnerabilityReferenceSerializer(many=True, source="vulnerabilityreference_set") exploits = V2ExploitSerializer(many=True, source="weaknesses") + severities = V2VulnerabilitySeveritySerializer(many=True) def get_aliases(self, vulnerability): return vulnerability.aliases.only("alias").values_list("alias", flat=True) @@ -145,11 +144,11 @@ class Meta: "vulnerability_id", "aliases", "status", - "scores", "weaknesses", "summary", "exploits", "references", + "severities", ) @@ -358,7 +357,7 @@ def get_queryset(self): .get_queryset() .prefetch_related( "weaknesses", - # "severities", + "severities", # "exploits", ) ) diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index e8361c092..c02a84a02 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -13,7 +13,6 @@ from rest_framework.response import Response from rest_framework.reverse import reverse -from vulnerabilities.api import VulnerabilitySeveritySerializer from vulnerabilities.models import Package from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityReference @@ -41,11 +40,24 @@ class Meta: fields = ["url", "reference_type", "reference_id"] +class VulnerabilitySeverityV2Serializer(serializers.ModelSerializer): + class Meta: + model = VulnerabilitySeverity + fields = ["url", "value", "scoring_system", "scoring_elements", "published_at"] + + def to_representation(self, instance): + data = super().to_representation(instance) + published_at = data.get("published_at", None) + if not published_at: + data.pop("published_at") + return data + + class VulnerabilityV2Serializer(serializers.ModelSerializer): aliases = serializers.SerializerMethodField() weaknesses = WeaknessV2Serializer(many=True) references = VulnerabilityReferenceV2Serializer(many=True, source="vulnerabilityreference_set") - severities = VulnerabilitySeveritySerializer(many=True) + severities = VulnerabilitySeverityV2Serializer(many=True) class Meta: model = Vulnerability @@ -61,9 +73,6 @@ class Meta: def get_aliases(self, obj): return [alias.alias for alias in obj.aliases.all()] - def get_severities(self, obj): - return obj.severities - class VulnerabilityListSerializer(serializers.ModelSerializer): url = serializers.SerializerMethodField() diff --git a/vulnerabilities/import_runner.py b/vulnerabilities/import_runner.py index 520dc09fb..0dcafda10 100644 --- a/vulnerabilities/import_runner.py +++ b/vulnerabilities/import_runner.py @@ -180,33 +180,36 @@ def process_inferences(inferences: List[Inference], advisory: Advisory, improver reference_id=ref.reference_id, url=ref.url, ) - if not reference: - continue - VulnerabilityRelatedReference.objects.update_or_create( - reference=reference, - vulnerability=vulnerability, - ) + if reference: + VulnerabilityRelatedReference.objects.update_or_create( + reference=reference, + vulnerability=vulnerability, + ) updated = False for severity in ref.severities: try: published_at = str(severity.published_at) if severity.published_at else None - _vs, updated = VulnerabilitySeverity.objects.update_or_create( + ( + vulnerability_severity, + updated, + ) = VulnerabilitySeverity.objects.update_or_create( scoring_system=severity.system.identifier, - reference=reference, + url=ref.url, + value=severity.value, + scoring_elements=severity.scoring_elements, defaults={ - "value": str(severity.value), - "scoring_elements": str(severity.scoring_elements), "published_at": published_at, }, ) + vulnerability.severities.add(vulnerability_severity) except: logger.error( f"Failed to create VulnerabilitySeverity for: {severity} with error:\n{traceback_format_exc()}" ) if updated: logger.info( - f"Severity updated for reference {ref!r} to value: {severity.value!r} " + f"Severity updated for reference {ref.url!r} to value: {severity.value!r} " f"and scoring_elements: {severity.scoring_elements!r}" ) diff --git a/vulnerabilities/management/commands/export.py b/vulnerabilities/management/commands/export.py index 3e8d5e6b6..08685e33d 100644 --- a/vulnerabilities/management/commands/export.py +++ b/vulnerabilities/management/commands/export.py @@ -22,20 +22,12 @@ def serialize_severity(sev): - # inlines refs - ref = sev.reference - sevref = { - "url": ref.url, - "reference_type": ref.reference_type, - "reference_id": ref.reference_id, - } - return { "score": sev.value, "scoring_system": sev.scoring_system, "scoring_elements": sev.scoring_elements, "published_at": sev.published_at, - "reference": sevref, + "url": sev.url, } @@ -44,7 +36,7 @@ def serialize_vulnerability(vuln): Return a plain data mapping seralized from ``vuln`` Vulnerability instance. """ aliases = list(vuln.aliases.values_list("alias", flat=True)) - severities = [serialize_severity(sev) for sev in vuln.severities] + severities = [serialize_severity(sev) for sev in vuln.severities.all()] weaknesses = [wkns.cwe for wkns in vuln.weaknesses.all()] references = list( @@ -161,11 +153,11 @@ def packages_by_type_ns_name(): "affected_by_vulnerabilities", "affected_by_vulnerabilities__references", "affected_by_vulnerabilities__weaknesses", - "affected_by_vulnerabilities__references__vulnerabilityseverity_set", + "affected_by_vulnerabilities__severities", "fixing_vulnerabilities", "fixing_vulnerabilities__references", "fixing_vulnerabilities__weaknesses", - "fixing_vulnerabilities__references__vulnerabilityseverity_set", + "fixing_vulnerabilities__severities", ) .paginated() ) diff --git a/vulnerabilities/migrations/0078_alter_vulnerabilityseverity_options_and_more.py b/vulnerabilities/migrations/0078_alter_vulnerabilityseverity_options_and_more.py new file mode 100644 index 000000000..c3e4aad38 --- /dev/null +++ b/vulnerabilities/migrations/0078_alter_vulnerabilityseverity_options_and_more.py @@ -0,0 +1,220 @@ +# Generated by Django 4.2.16 on 2024-11-01 12:40 + +from django.db import migrations, models +from aboutcode.pipeline import LoopProgress +from datetime import datetime +from datetime import timezone + + +class Migration(migrations.Migration): + """ + Add url field to the VulnerabilitySeverity model and remove reference foreignkey + relationship. Also, add new M2M 'severities' field in Vulnerability and + AffectedByPackageRelatedVulnerability relationship. + + To achieve this following model changes and data migrations are applied in chronological order: + - Make VulnerabilitySeverity reference field nullable (to make the migration reversible). + - Add 'severities' field to AffectedByPackageRelatedVulnerability. + - Add 'severities' field to Vulnerability. + - Add 'url' field to VulnerabilitySeverity. + - Data migration to remove corrupted SUSE scores. + - Data migration to enable reprocessing of old suse Advisory on next import. + - Data migration to populate new VulnerabilitySeverity url field using reference. + - Data migration to populate Vulnerability 'severities' M2M relationship. + - Delete VulnerabilitySeverity reference field. + """ + + def remove_inaccurate_suse_score(apps, schema_editor): + """ + Remove inaccurate suse severity scores. + See https://github.com/aboutcode-org/vulnerablecode/issues/1597 + """ + print(f"\nRemoving inaccurate suse severity scores.") + VulnerabilitySeverity = apps.get_model("vulnerabilities", "VulnerabilitySeverity") + VulnerabilitySeverity.objects.filter( + reference__url="https://ftp.suse.com/pub/projects/security/yaml/suse-cvss-scores.yaml" + ).delete() + + def reverse_remove_inaccurate_suse_score(apps, schema_editor): + """Reverse data migration not needed for inaccurate severity scores.""" + pass + + def reprocess_suse_advisory_on_next_import(apps, schema_editor): + """Clear `date_imported` on old suse advisory to enable reprocessing on next suse import.""" + + print(f"\nEnable reprocess of old suse advisory on next import.") + Advisory = apps.get_model("vulnerabilities", "Advisory") + Advisory.objects.filter( + created_by="vulnerabilities.importers.suse_scores.SUSESeverityScoreImporter" + ).update(date_imported=None) + + def reverse_reprocess_suse_advisory_on_next_import(apps, schema_editor): + """Populate `date_imported` on old suse advisory to prevent reprocessing on next suse import.""" + Advisory = apps.get_model("vulnerabilities", "Advisory") + Advisory.objects.filter( + created_by="vulnerabilities.importers.suse_scores.SUSESeverityScoreImporter" + ).update(date_imported=datetime.now(timezone.utc)) + + def populate_severity_model_with_url(apps, schema_editor): + """Populate the new VulnerabilitySeverity `url` field using reference url""" + + VulnerabilitySeverity = apps.get_model("vulnerabilities", "VulnerabilitySeverity") + chunk_size = 10000 + batch = [] + vulnerability_severity_query = VulnerabilitySeverity.objects.select_related("reference") + + print( + f"\nPopulating {vulnerability_severity_query.count():,d} VulnerabilitySeverity" + "`url` field using reference url." + ) + progress = LoopProgress( + total_iterations=vulnerability_severity_query.count(), + progress_step=10, + logger=print, + ) + for severity in progress.iter(vulnerability_severity_query.iterator(chunk_size=chunk_size)): + severity.url = severity.reference.url + batch.append(severity) + + if len(batch) >= chunk_size: + VulnerabilitySeverity.objects.bulk_update( + objs=batch, + fields=["url"], + ) + batch.clear() + + VulnerabilitySeverity.objects.bulk_update( + objs=batch, + fields=["url"], + ) + + def reverse_populate_severity_model_reference_using_url(apps, schema_editor): + """Reverse: Populate the reference using `url` to ensure proper reverse migration.""" + VulnerabilitySeverity = apps.get_model("vulnerabilities", "VulnerabilitySeverity") + VulnerabilityReference = apps.get_model("vulnerabilities", "VulnerabilityReference") + chunk_size = 10000 + batch = [] + vulnerability_severity_query = VulnerabilitySeverity.objects.select_related("reference") + + print( + f"\nReverse: Populating {vulnerability_severity_query.count():,d} VulnerabilitySeverity" + "`reference` relation using url." + ) + progress = LoopProgress( + total_iterations=vulnerability_severity_query.count(), + progress_step=10, + logger=print, + ) + for severity in progress.iter(vulnerability_severity_query.iterator(chunk_size=chunk_size)): + severity.reference = VulnerabilityReference.objects.get(url=severity.url) + batch.append(severity) + + if len(batch) >= chunk_size: + VulnerabilitySeverity.objects.bulk_update( + objs=batch, + fields=["reference"], + ) + batch.clear() + + VulnerabilitySeverity.objects.bulk_update( + objs=batch, + fields=["reference"], + ) + + def populate_vulnerability_model_with_severities(apps, schema_editor): + """Populate the new Vulnerability `severities` relation using referenced severity.""" + Vulnerability = apps.get_model("vulnerabilities", "Vulnerability") + chunk_size = 10000 + + vulnerability_query = Vulnerability.objects.prefetch_related( + "references__vulnerabilityseverity_set" + ) + print( + f"\nPopulating {vulnerability_query.count():,d} Vulnerability `severities`" + "relation using referenced severity." + ) + progress = LoopProgress( + total_iterations=vulnerability_query.count(), + progress_step=10, + logger=print, + ) + for vulnerability in progress.iter(vulnerability_query.iterator(chunk_size=chunk_size)): + references = vulnerability.references.all() + severity_ids = [ + severity.id + for reference in references + for severity in reference.vulnerabilityseverity_set.all() + if reference.vulnerabilityseverity_set.exists() + ] + vulnerability.severities.set(severity_ids) + + def reverse_populate_vulnerability_model_with_severities(apps, schema_editor): + """Reverse data migration not needed for new `severities` relationship.""" + pass + + dependencies = [ + ("vulnerabilities", "0077_alter_packagechangelog_software_version_and_more"), + ] + + operations = [ + # reference field should nullable to properly support reverse migration. + migrations.AlterField( + model_name="vulnerabilityseverity", + name="reference", + field=models.ForeignKey( + to="vulnerabilities.vulnerabilityreference", + on_delete=models.CASCADE, + null=True, + ), + ), + migrations.AlterModelOptions( + name="vulnerabilityseverity", + options={"ordering": ["url", "scoring_system", "value"]}, + ), + migrations.AlterUniqueTogether( + name="vulnerabilityseverity", + unique_together=set(), + ), + migrations.AddField( + model_name="affectedbypackagerelatedvulnerability", + name="severities", + field=models.ManyToManyField( + related_name="affected_package_vulnerability_relations", + to="vulnerabilities.vulnerabilityseverity", + ), + ), + migrations.AddField( + model_name="vulnerability", + name="severities", + field=models.ManyToManyField( + related_name="vulnerabilities", to="vulnerabilities.vulnerabilityseverity" + ), + ), + migrations.AddField( + model_name="vulnerabilityseverity", + name="url", + field=models.URLField( + help_text="URL to the vulnerability severity", max_length=1024, null=True + ), + ), + migrations.RunPython( + code=remove_inaccurate_suse_score, + reverse_code=reverse_remove_inaccurate_suse_score, + ), + migrations.RunPython( + code=reprocess_suse_advisory_on_next_import, + reverse_code=reverse_reprocess_suse_advisory_on_next_import, + ), + migrations.RunPython( + code=populate_severity_model_with_url, + reverse_code=reverse_populate_severity_model_reference_using_url, + ), + migrations.RunPython( + code=populate_vulnerability_model_with_severities, + reverse_code=reverse_populate_vulnerability_model_with_severities, + ), + migrations.RemoveField( + model_name="vulnerabilityseverity", + name="reference", + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index b95a07297..c62949992 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -156,6 +156,42 @@ def with_package_counts(self): ) +class VulnerabilitySeverity(models.Model): + url = models.URLField( + max_length=1024, + null=True, + help_text="URL to the vulnerability severity", + ) + + scoring_system_choices = tuple( + (system.identifier, system.name) for system in SCORING_SYSTEMS.values() + ) + + scoring_system = models.CharField( + max_length=50, + choices=scoring_system_choices, + help_text="Identifier for the scoring system used. Available choices are: {} ".format( + ",\n".join(f"{sid}: {sname}" for sid, sname in scoring_system_choices) + ), + ) + + value = models.CharField(max_length=50, help_text="Example: 9.0, Important, High") + + scoring_elements = models.CharField( + max_length=150, + null=True, + help_text="Supporting scoring elements used to compute the score values. " + "For example a CVSS vector string as used to compute a CVSS score.", + ) + + published_at = models.DateTimeField( + blank=True, null=True, help_text="UTC Date of publication of the vulnerability severity" + ) + + class Meta: + ordering = ["url", "scoring_system", "value"] + + class VulnerabilityStatusType(models.IntegerChoices): """List of vulnerability statuses.""" @@ -202,6 +238,11 @@ class Vulnerability(models.Model): choices=VulnerabilityStatusType.choices, default=VulnerabilityStatusType.PUBLISHED ) + severities = models.ManyToManyField( + VulnerabilitySeverity, + related_name="vulnerabilities", + ) + objects = VulnerabilityQuerySet.as_manager() class Meta: @@ -215,13 +256,6 @@ def __str__(self): def vcid(self): return self.vulnerability_id - @property - def severities(self): - """ - Return a queryset of VulnerabilitySeverity for this vulnerability. - """ - return VulnerabilitySeverity.objects.filter(reference__in=self.references.all()) - @property def affected_packages(self): """ @@ -986,41 +1020,14 @@ class Meta(PackageRelatedVulnerabilityBase.Meta): class AffectedByPackageRelatedVulnerability(PackageRelatedVulnerabilityBase): - class Meta(PackageRelatedVulnerabilityBase.Meta): - verbose_name_plural = "Affected By Package Related Vulnerabilities" - - -class VulnerabilitySeverity(models.Model): - reference = models.ForeignKey(VulnerabilityReference, on_delete=models.CASCADE) - scoring_system_choices = tuple( - (system.identifier, system.name) for system in SCORING_SYSTEMS.values() + severities = models.ManyToManyField( + VulnerabilitySeverity, + related_name="affected_package_vulnerability_relations", ) - scoring_system = models.CharField( - max_length=50, - choices=scoring_system_choices, - help_text="Identifier for the scoring system used. Available choices are: {} ".format( - ",\n".join(f"{sid}: {sname}" for sid, sname in scoring_system_choices) - ), - ) - - value = models.CharField(max_length=50, help_text="Example: 9.0, Important, High") - - scoring_elements = models.CharField( - max_length=150, - null=True, - help_text="Supporting scoring elements used to compute the score values. " - "For example a CVSS vector string as used to compute a CVSS score.", - ) - - published_at = models.DateTimeField( - blank=True, null=True, help_text="UTC Date of publication of the vulnerability severity" - ) - - class Meta: - unique_together = ["reference", "scoring_system", "value"] - ordering = ["reference", "scoring_system", "value"] + class Meta(PackageRelatedVulnerabilityBase.Meta): + verbose_name_plural = "Affected By Package Related Vulnerabilities" class AliasQuerySet(BaseQuerySet): diff --git a/vulnerabilities/pipelines/compute_package_risk.py b/vulnerabilities/pipelines/compute_package_risk.py index e5b48ea0e..c304d2838 100644 --- a/vulnerabilities/pipelines/compute_package_risk.py +++ b/vulnerabilities/pipelines/compute_package_risk.py @@ -29,19 +29,28 @@ def steps(cls): return (cls.add_package_risk_score,) def add_package_risk_score(self): - affected_packages = Package.objects.filter( - affected_by_vulnerabilities__isnull=False + affected_packages = ( + Package.objects.filter(affected_by_vulnerabilities__isnull=False).prefetch_related( + "affectedbypackagerelatedvulnerability_set__vulnerability", + "affectedbypackagerelatedvulnerability_set__vulnerability__references", + "affectedbypackagerelatedvulnerability_set__vulnerability__severities", + "affectedbypackagerelatedvulnerability_set__vulnerability__exploits", + ) ).distinct() self.log(f"Calculating risk for {affected_packages.count():,d} affected package records") - progress = LoopProgress(total_iterations=affected_packages.count(), logger=self.log) + progress = LoopProgress( + total_iterations=affected_packages.count(), + logger=self.log, + progress_step=5, + ) updatables = [] updated_package_count = 0 - batch_size = 5000 + batch_size = 10000 - for package in progress.iter(affected_packages.paginated()): + for package in progress.iter(affected_packages.paginated(per_page=batch_size)): risk_score = compute_package_risk(package) if not risk_score: diff --git a/vulnerabilities/pipes/advisory.py b/vulnerabilities/pipes/advisory.py index 819744710..6637122a3 100644 --- a/vulnerabilities/pipes/advisory.py +++ b/vulnerabilities/pipes/advisory.py @@ -104,25 +104,24 @@ def import_advisory( reference_id=ref.reference_id, url=ref.url, ) - if not reference: - continue - - VulnerabilityRelatedReference.objects.update_or_create( - reference=reference, - vulnerability=vulnerability, - ) + if reference: + VulnerabilityRelatedReference.objects.update_or_create( + reference=reference, + vulnerability=vulnerability, + ) for severity in ref.severities: try: published_at = str(severity.published_at) if severity.published_at else None - _, created = VulnerabilitySeverity.objects.update_or_create( + vulnerability_severity, created = VulnerabilitySeverity.objects.update_or_create( scoring_system=severity.system.identifier, - reference=reference, + url=ref.url, + value=severity.value, + scoring_elements=severity.scoring_elements, defaults={ - "value": str(severity.value), - "scoring_elements": str(severity.scoring_elements), "published_at": published_at, }, ) + vulnerability.severities.add(vulnerability_severity) except: if logger: logger( @@ -132,7 +131,7 @@ def import_advisory( if not created: if logger: logger( - f"Severity updated for reference {ref!r} to value: {severity.value!r} " + f"Severity updated for reference {ref.url!r} to value: {severity.value!r} " f"and scoring_elements: {severity.scoring_elements!r}", level=logging.DEBUG, ) diff --git a/vulnerabilities/risk.py b/vulnerabilities/risk.py index 9eb4ac6ec..8c4cb6c49 100644 --- a/vulnerabilities/risk.py +++ b/vulnerabilities/risk.py @@ -10,10 +10,6 @@ from urllib.parse import urlparse -from vulnerabilities.models import AffectedByPackageRelatedVulnerability -from vulnerabilities.models import Exploit -from vulnerabilities.models import Package -from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityReference from vulnerabilities.severity_systems import EPSS from vulnerabilities.weight_config import WEIGHT_CONFIG @@ -40,7 +36,7 @@ def get_weighted_severity(severities): score_list = [] for severity in severities: - parsed_url = urlparse(severity.reference.url) + parsed_url = urlparse(severity.url) severity_source = parsed_url.netloc.replace("www.", "", 1) weight = WEIGHT_CONFIG.get(severity_source, DEFAULT_WEIGHT) max_weight = float(weight) / 10 @@ -72,25 +68,22 @@ def get_exploitability_level(exploits, references, severities): elif severities: # high EPSS. - epss = severities.filter( - scoring_system=EPSS.identifier, - ) - epss = any(float(epss.value) > 0.8 for epss in epss) - if epss: - exploit_level = 2 + for severity in severities: + if severity.scoring_system == EPSS.identifier and float(severity.value) > 0.8: + exploit_level = 2 + break elif references: # PoC/Exploit script published - ref_exploits = references.filter( - reference_type=VulnerabilityReference.EXPLOIT, - ) - if ref_exploits: - exploit_level = 1 + for reference in references: + if reference.reference_type == VulnerabilityReference.EXPLOIT: + exploit_level = 1 + break return exploit_level -def compute_vulnerability_risk(vulnerability: Vulnerability): +def compute_vulnerability_risk(vulnerability): """ Risk may be expressed as a number ranging from 0 to 10. Risk is calculated from weighted severity and exploitability values. @@ -98,26 +91,24 @@ def compute_vulnerability_risk(vulnerability: Vulnerability): Risk = min(weighted severity * exploitability, 10) """ - references = vulnerability.references - severities = vulnerability.severities.select_related("reference") - exploits = Exploit.objects.filter(vulnerability=vulnerability) - if references.exists() or severities.exists() or exploits.exists(): + severities = vulnerability.severities.all() + exploits = vulnerability.exploits.all() + reference = vulnerability.references.all() + if reference.exists() or severities.exists() or exploits.exists(): weighted_severity = get_weighted_severity(severities) - exploitability = get_exploitability_level(exploits, references, severities) + exploitability = get_exploitability_level(exploits, reference, severities) return min(weighted_severity * exploitability, 10) -def compute_package_risk(package: Package): +def compute_package_risk(package): """ Calculate the risk for a package by iterating over all vulnerabilities that affects this package and determining the associated risk. """ result = [] - for pkg_related_vul in AffectedByPackageRelatedVulnerability.objects.filter( - package=package - ).prefetch_related("vulnerability"): - if risk := compute_vulnerability_risk(pkg_related_vul.vulnerability): + for package_vulnerability in package.affectedbypackagerelatedvulnerability_set.all(): + if risk := compute_vulnerability_risk(package_vulnerability.vulnerability): result.append(risk) if not result: diff --git a/vulnerabilities/templates/vulnerability_details.html b/vulnerabilities/templates/vulnerability_details.html index d1f2fb6de..402855169 100644 --- a/vulnerabilities/templates/vulnerability_details.html +++ b/vulnerabilities/templates/vulnerability_details.html @@ -139,13 +139,9 @@ {{ severity.scoring_system }} {{ severity.value }} - {% if severity.reference.url %} - - {{ severity.reference.url }} + + {{ severity.url }} - {% else %} - {{ severity.reference.reference_id }} - {% endif %} {% empty %} diff --git a/vulnerabilities/tests/test_api.py b/vulnerabilities/tests/test_api.py index 926d3c219..ac10d1b7c 100644 --- a/vulnerabilities/tests/test_api.py +++ b/vulnerabilities/tests/test_api.py @@ -222,8 +222,8 @@ def setUp(self): url="https://.com", ) - VulnerabilitySeverity.objects.create( - reference=self.reference1, + severity = VulnerabilitySeverity.objects.create( + url="https://.com", scoring_system=EPSS.identifier, scoring_elements=".0016", value="0.526", @@ -239,6 +239,7 @@ def setUp(self): cwe_id=10000 ) # cwe not present in weaknesses_db self.invalid_weaknesses.vulnerabilities.add(self.vulnerability) + self.vulnerability.severities.add(severity) def test_api_status(self): response = self.csrf_client.get("/api/vulnerabilities/") diff --git a/vulnerabilities/tests/test_api_extension.py b/vulnerabilities/tests/test_api_extension.py index d652fa203..fc971a576 100644 --- a/vulnerabilities/tests/test_api_extension.py +++ b/vulnerabilities/tests/test_api_extension.py @@ -45,7 +45,7 @@ def vulnerability_severity(vulnerability_reference): scoring_system="cvssv3_vector", value="7.0", scoring_elements="CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", - reference_id=vulnerability_reference.id, + url=f"https://..", ) @@ -86,7 +86,7 @@ def test_V2VulnerabilitySeveritySerializer(vulnerability_severity): results = V2VulnerabilitySeveritySerializer(instance=vulnerability_severity).data expected = { "published_at": None, - "reference": {"reference_id": "fake", "reference_type": "", "reference_url": "https://.."}, + "url": "https://..", "score": "7.0", "scoring_elements": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "scoring_system": "cvssv3_vector", diff --git a/vulnerabilities/tests/test_data/export_command/aboutcode-vulnerabilities/ps/VCID-pst6-b358-aaap.yml b/vulnerabilities/tests/test_data/export_command/aboutcode-vulnerabilities/ps/VCID-pst6-b358-aaap.yml index 07adaf90e..63ab7f5af 100644 --- a/vulnerabilities/tests/test_data/export_command/aboutcode-vulnerabilities/ps/VCID-pst6-b358-aaap.yml +++ b/vulnerabilities/tests/test_data/export_command/aboutcode-vulnerabilities/ps/VCID-pst6-b358-aaap.yml @@ -7,10 +7,7 @@ severities: scoring_system: cvssv3_vector scoring_elements: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H published_at: - reference: - url: https://.. - reference_type: - reference_id: fake + url: https://.. weaknesses: - CWE-15 references: diff --git a/vulnerabilities/tests/test_export.py b/vulnerabilities/tests/test_export.py index df9233b80..244940261 100644 --- a/vulnerabilities/tests/test_export.py +++ b/vulnerabilities/tests/test_export.py @@ -50,7 +50,7 @@ def vulnerability_severity(vulnerability_reference): scoring_system="cvssv3_vector", value="7.0", scoring_elements="CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", - reference_id=vulnerability_reference.id, + url=f"https://..", ) @@ -66,6 +66,7 @@ def vulnerability(db, vulnerability_reference, vulnerability_severity): weakness = Weakness.objects.create(cwe_id=15) vulnerability.weaknesses.add(weakness) + vulnerability.severities.add(vulnerability_severity) return vulnerability diff --git a/vulnerabilities/tests/test_risk.py b/vulnerabilities/tests/test_risk.py index 96d9f9445..6aa50d2ac 100644 --- a/vulnerabilities/tests/test_risk.py +++ b/vulnerabilities/tests/test_risk.py @@ -29,25 +29,20 @@ def vulnerability(): vul = Vulnerability(vulnerability_id="VCID-Existing") vul.save() - reference1 = VulnerabilityReference.objects.create( - reference_id="", + severity1 = VulnerabilitySeverity.objects.create( url="https://nvd.nist.gov/vuln/detail/CVE-xxxx-xxx1", - ) - - VulnerabilitySeverity.objects.create( - reference=reference1, scoring_system=CVSSV3.identifier, scoring_elements="CVSS:3.0/AV:P/AC:H/PR:H/UI:R/S:C/C:H/I:H/A:N/E:H/RL:O/RC:R/CR:H/MAC:H/MC:L", value="6.5", ) - VulnerabilitySeverity.objects.create( - reference=reference1, + severity2 = VulnerabilitySeverity.objects.create( + url="https://nvd.nist.gov/vuln/detail/CVE-xxxx-xxx1", scoring_system=GENERIC.identifier, value="MODERATE", # 6.9 ) - - VulnerabilityRelatedReference.objects.create(reference=reference1, vulnerability=vul) + vul.severities.add(severity1) + vul.severities.add(severity2) weaknesses = Weakness.objects.create(cwe_id=119) vul.weaknesses.add(weaknesses) @@ -84,19 +79,14 @@ def high_epss_score(): vul = Vulnerability(vulnerability_id="VCID-HIGH-EPSS") vul.save() - reference1 = VulnerabilityReference.objects.create( - reference_id="", + severity = VulnerabilitySeverity.objects.create( url="https://nvd.nist.gov/vuln/detail/CVE-xxxx-xxx3", - ) - - VulnerabilitySeverity.objects.create( - reference=reference1, scoring_system=EPSS.identifier, value=".9", ) + vul.severities.add(severity) - VulnerabilityRelatedReference.objects.create(reference=reference1, vulnerability=vul) - return vul.severities + return vul.severities.all() @pytest.fixture @@ -105,28 +95,19 @@ def low_epss_score(): vul = Vulnerability(vulnerability_id="VCID-LOW-EPSS") vul.save() - reference1 = VulnerabilityReference.objects.create( - reference_id="", + severity = VulnerabilitySeverity.objects.create( url="https://nvd.nist.gov/vuln/detail/CVE-xxxx-xxx4", - ) - - VulnerabilitySeverity.objects.create( - reference=reference1, scoring_system=EPSS.identifier, value=".3", ) + vul.severities.add(severity) - VulnerabilityRelatedReference.objects.create(reference=reference1, vulnerability=vul) - return vul.severities + return vul.severities.all() @pytest.mark.django_db def test_exploitability_level( - exploit, - vulnerability_with_exploit_ref, - high_epss_score, - low_epss_score, - vulnerability, + exploit, vulnerability_with_exploit_ref, high_epss_score, low_epss_score ): assert get_exploitability_level(exploit, None, None) == 2 @@ -137,9 +118,9 @@ def test_exploitability_level( assert ( get_exploitability_level( - None, - vulnerability_with_exploit_ref.references, - vulnerability_with_exploit_ref.severities, + exploits=None, + references=vulnerability_with_exploit_ref.references.all(), + severities=vulnerability_with_exploit_ref.severities.all(), ) == 1 ) @@ -152,18 +133,13 @@ def test_get_weighted_severity(vulnerability): severities = vulnerability.severities.all() assert get_weighted_severity(severities) == 6.210000000000001 - reference2 = VulnerabilityReference.objects.create( - reference_id="", + severity2 = VulnerabilitySeverity.objects.create( url="https://security-tracker.debian.org/tracker/CVE-2019-13057", - ) - - VulnerabilitySeverity.objects.create( - reference=reference2, scoring_system=GENERIC.identifier, value="CRITICAL", ) + vulnerability.severities.add(severity2) - VulnerabilityRelatedReference.objects.create(reference=reference2, vulnerability=vulnerability) new_severities = vulnerability.severities.all() assert get_weighted_severity(new_severities) == 7 diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 51cdcd049..c51e77e93 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -165,7 +165,7 @@ def get_context_data(self, **kwargs): severity_vectors = [] severity_values = set() - for s in self.object.severities: + for s in self.object.severities.all(): if s.scoring_system == EPSS.identifier: continue @@ -214,7 +214,7 @@ def get_context_data(self, **kwargs): { "vulnerability": self.object, "vulnerability_search_form": VulnerabilitySearchForm(self.request.GET), - "severities": list(self.object.severities), + "severities": list(self.object.severities.all()), "severity_score_range": get_severity_range(severity_values), "severity_vectors": severity_vectors, "references": self.object.references.all(),