diff --git a/PIPELINES-AVID.rst b/PIPELINES-AVID.rst index 7c1ec7948..de6f399d5 100644 --- a/PIPELINES-AVID.rst +++ b/PIPELINES-AVID.rst @@ -96,9 +96,6 @@ The following table describes how each pipeline constructs its AVID. * - ruby_importer_v2 - ruby_advisory_db - {file_id} - * - epss_importer_v2 - - epss - - CVE ID of the record * - gentoo_importer_v2 - gentoo - GLSA ID of the record diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py index a33e6d714..1774b1150 100644 --- a/vulnerabilities/importers/__init__.py +++ b/vulnerabilities/importers/__init__.py @@ -20,7 +20,6 @@ from vulnerabilities.pipelines.v2_importers import ( elixir_security_importer as elixir_security_importer_v2, ) -from vulnerabilities.pipelines.v2_importers import epss_importer_v2 from vulnerabilities.pipelines.v2_importers import fireeye_importer_v2 from vulnerabilities.pipelines.v2_importers import gentoo_importer as gentoo_importer_v2 from vulnerabilities.pipelines.v2_importers import github_osv_importer as github_osv_importer_v2 @@ -77,7 +76,6 @@ project_kb_statements_importer_v2.ProjectKBStatementsPipeline, project_kb_msr2019_importer_v2.ProjectKBMSR2019Pipeline, ruby_importer_v2.RubyImporterPipeline, - epss_importer_v2.EPSSImporterPipeline, gentoo_importer_v2.GentooImporterPipeline, nginx_importer_v2.NginxImporterPipeline, debian_importer_v2.DebianImporterPipeline, diff --git a/vulnerabilities/importers/epss.py b/vulnerabilities/importers/epss.py deleted file mode 100644 index 982229e09..000000000 --- a/vulnerabilities/importers/epss.py +++ /dev/null @@ -1,67 +0,0 @@ -# -# Copyright (c) nexB Inc. and others. All rights reserved. -# VulnerableCode is a trademark of nexB Inc. -# SPDX-License-Identifier: Apache-2.0 -# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. -# See https://github.com/aboutcode-org/vulnerablecode for support or download. -# See https://aboutcode.org for more information about nexB OSS projects. -# -import csv -import gzip -import logging -import urllib.request -from datetime import datetime -from typing import Iterable - -from vulnerabilities import severity_systems -from vulnerabilities.importer import AdvisoryData -from vulnerabilities.importer import Importer -from vulnerabilities.importer import Reference -from vulnerabilities.importer import VulnerabilitySeverity - -logger = logging.getLogger(__name__) - - -class EPSSImporter(Importer): - """Exploit Prediction Scoring System (EPSS) Importer""" - - advisory_url = "https://epss.cyentia.com/epss_scores-current.csv.gz" - spdx_license_expression = "unknown" - importer_name = "EPSS Importer" - - def advisory_data(self) -> Iterable[AdvisoryData]: - response = urllib.request.urlopen(self.advisory_url) - with gzip.open(response, "rb") as f: - lines = [l.decode("utf-8") for l in f.readlines()] - - epss_reader = csv.reader(lines) - model_version, score_date = next( - epss_reader - ) # score_date='score_date:2024-05-19T00:00:00+0000' - published_at = datetime.strptime(score_date[11::], "%Y-%m-%dT%H:%M:%S%z") - - next(epss_reader) # skip the header row - for epss_row in epss_reader: - cve, score, percentile = epss_row - - if not cve or not score or not percentile: - logger.error(f"Invalid epss row: {epss_row}") - continue - - severity = VulnerabilitySeverity( - system=severity_systems.EPSS, - value=score, - scoring_elements=percentile, - published_at=published_at, - ) - - references = Reference( - url=f"https://api.first.org/data/v1/epss?cve={cve}", - severities=[severity], - ) - - yield AdvisoryData( - aliases=[cve], - references=[references], - url=self.advisory_url, - ) diff --git a/vulnerabilities/improvers/__init__.py b/vulnerabilities/improvers/__init__.py index 51694bde9..32265526b 100644 --- a/vulnerabilities/improvers/__init__.py +++ b/vulnerabilities/improvers/__init__.py @@ -17,6 +17,7 @@ from vulnerabilities.pipelines.v2_improvers import ( enhance_with_metasploit as enhance_with_metasploit_v2, ) +from vulnerabilities.pipelines.v2_improvers import epss_pipeline from vulnerabilities.pipelines.v2_improvers import flag_ghost_packages as flag_ghost_packages_v2 from vulnerabilities.pipelines.v2_improvers import ( group_advisories_for_packages as group_advisories_for_packages_v2, @@ -43,5 +44,6 @@ enhance_with_github_poc.GithubPocsImproverPipeline, mark_unfurl_version_range.MarkUnfurlVersionRangePipeline, group_advisories_for_packages_v2.GroupAdvisoriesForPackages, + epss_pipeline.EPSSPipeline, ] ) diff --git a/vulnerabilities/migrations/0145_epss_score_model.py b/vulnerabilities/migrations/0145_epss_score_model.py new file mode 100644 index 000000000..6df56d2ed --- /dev/null +++ b/vulnerabilities/migrations/0145_epss_score_model.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.11 on 2026-10-28 10:46 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vulnerabilities', '0144_remove_advisoryv2_advisory_latest_by_avid_idx_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='EPSSScore', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(help_text='Date for which this EPSS score was published')), + ('score', models.DecimalField(decimal_places=6, help_text='EPSS probability score', max_digits=7)), + ('percentile', models.DecimalField(decimal_places=6, help_text='EPSS percentile ranking', max_digits=7)), + ('is_latest', models.BooleanField(default=False, help_text='Indicates whether this is the latest EPSS score')), + ('alias', models.ForeignKey(help_text='The CVE alias associated with this EPSS score', on_delete=django.db.models.deletion.CASCADE, related_name='epss_scores', to='vulnerabilities.advisoryalias')), + ], + options={ + 'unique_together': {('alias', 'date')}, + }, + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 521f0ca6a..9e1da059d 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -2837,6 +2837,43 @@ def url(self): return f"https://github.com/nodejs/security-wg/blob/main/vuln/npm/{id}.json" +class EPSSScore(models.Model): + """ + Tracks daily EPSS (Exploit Prediction Scoring System) scores for CVE aliases. + """ + + alias = models.ForeignKey( + AdvisoryAlias, + related_name="epss_scores", + on_delete=models.CASCADE, + help_text="The CVE alias associated with this EPSS score", + ) + + date = models.DateField( + help_text="Date for which this EPSS score was published", + ) + + score = models.DecimalField( + max_digits=7, + decimal_places=6, + help_text="EPSS probability score", + ) + + percentile = models.DecimalField( + max_digits=7, + decimal_places=6, + help_text="EPSS percentile ranking", + ) + + is_latest = models.BooleanField( + default=False, + help_text="Indicates whether this is the latest EPSS score", + ) + + class Meta: + unique_together = (("alias", "date"),) + + class Patch(models.Model): """ A generic patch linked to an advisory. diff --git a/vulnerabilities/pipelines/v2_importers/epss_importer_v2.py b/vulnerabilities/pipelines/v2_importers/epss_importer_v2.py deleted file mode 100644 index 8f3a57d95..000000000 --- a/vulnerabilities/pipelines/v2_importers/epss_importer_v2.py +++ /dev/null @@ -1,90 +0,0 @@ -# -# Copyright (c) nexB Inc. and others. All rights reserved. -# VulnerableCode is a trademark of nexB Inc. -# SPDX-License-Identifier: Apache-2.0 -# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. -# See https://github.com/aboutcode-org/vulnerablecode for support or download. -# See https://aboutcode.org for more information about nexB OSS projects. -# -import csv -import gzip -import logging -import urllib.request -from datetime import datetime -from typing import Iterable - -from vulnerabilities import severity_systems -from vulnerabilities.importer import AdvisoryDataV2 -from vulnerabilities.importer import ReferenceV2 -from vulnerabilities.importer import VulnerabilitySeverity -from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2 - -logger = logging.getLogger(__name__) - - -class EPSSImporterPipeline(VulnerableCodeBaseImporterPipelineV2): - """Exploit Prediction Scoring System (EPSS) Importer""" - - advisory_url = "https://epss.cyentia.com/epss_scores-current.csv.gz" - pipeline_id = "epss_importer_v2" - spdx_license_expression = "unknown" - importer_name = "EPSS Importer" - datasource_id = "epss" - - exclude_from_package_todo = True - - precedence = 200 - - def advisories_count(self): - return len(self.lines) - - @classmethod - def steps(cls): - return ( - cls.fetch_db, - cls.collect_and_store_advisories, - ) - - def fetch_db(self): - logger.info(f"Fetching EPSS database from {self.advisory_url}") - response = urllib.request.urlopen(self.advisory_url) - with gzip.open(response, "rb") as f: - self.lines = [l.decode("utf-8") for l in f.readlines()] - - def collect_advisories(self) -> Iterable[AdvisoryDataV2]: - if not self.lines: - logger.error("No EPSS data loaded") - raise ValueError("EPSS data is empty") - - epss_reader = csv.reader(self.lines) - model_version, score_date = next( - epss_reader - ) # score_date='score_date:2024-05-19T00:00:00+0000' - published_at = datetime.strptime(score_date[11::], "%Y-%m-%dT%H:%M:%S%z") - - next(epss_reader) # skip the header row - for epss_row in epss_reader: - cve, score, percentile = epss_row - - if not cve or not score or not percentile: - logger.error(f"Invalid epss row: {epss_row}") - continue - - severity = VulnerabilitySeverity( - system=severity_systems.EPSS, - value=score, - scoring_elements=percentile, - published_at=published_at, - ) - - references = ReferenceV2( - url=f"https://api.first.org/data/v1/epss?cve={cve}", - ) - - yield AdvisoryDataV2( - advisory_id=cve, - severities=[severity], - references=[references], - url=self.advisory_url, - original_advisory_text=",".join(epss_row), - ) diff --git a/vulnerabilities/pipelines/v2_improvers/epss_pipeline.py b/vulnerabilities/pipelines/v2_improvers/epss_pipeline.py new file mode 100644 index 000000000..f616d037e --- /dev/null +++ b/vulnerabilities/pipelines/v2_improvers/epss_pipeline.py @@ -0,0 +1,127 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/aboutcode-org/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +import csv +import gzip +import urllib.request +from datetime import datetime +from datetime import timedelta +from decimal import Decimal + +from django.utils import timezone + +from vulnerabilities.models import AdvisoryAlias +from vulnerabilities.models import EPSSScore +from vulnerabilities.models import PipelineSchedule +from vulnerabilities.pipelines import VulnerableCodePipeline + + +class EPSSPipeline(VulnerableCodePipeline): + """ + Exploit Prediction Scoring System (EPSS) Pipeline. + + Downloads the latest daily EPSS CSV and bulk-inserts scores into EPSSScore model. Prunes scores older than the retention window. + """ + + pipeline_id = "epss_pipeline" + advisory_url = "https://epss.cyentia.com/epss_scores-current.csv.gz" + spdx_license_expression = "unknown" + + # Run pipeline every day. + run_interval = 1440 + run_priority = PipelineSchedule.ExecutionPriority.HIGH + retention_days = 90 # Keep 90 days of EPSS scores + batch_size = 10_000 + + @classmethod + def steps(cls): + return ( + cls.fetch_and_bulk_load_scores, + cls.prune_old_scores, + ) + + def fetch_and_bulk_load_scores(self): + self.log(f"Fetching EPSS data from {self.advisory_url}") + response = urllib.request.urlopen(self.advisory_url) + + with gzip.open(response, "rt", encoding="utf-8") as gzip_stream: + epss_csv_reader = csv.reader(gzip_stream) + + # Extract date from metadata header + _, date_header = next(epss_csv_reader) + score_date = datetime.fromisoformat(date_header.removeprefix("score_date:")).date() + + # Skip column headers + next(epss_csv_reader) + + scores_by_cve = { + cve: (epss_score, percentile) for cve, epss_score, percentile in epss_csv_reader + } + + self.log(f"Parsed {len(scores_by_cve):,d} EPSS scores for {score_date}.") + + # Map CVEs to ``AdvisoryAlias`` IDs and create any missing aliases + cve_to_alias_id = dict( + AdvisoryAlias.objects.filter(alias__in=scores_by_cve.keys()).values_list("alias", "id") + ) + missing_cves = set(scores_by_cve) - set(cve_to_alias_id) + if missing_cves: + self.log(f"Creating {len(missing_cves):,d} new CVE records") + new_aliases = [AdvisoryAlias(alias=cve) for cve in missing_cves] + AdvisoryAlias.objects.bulk_create( + new_aliases, + batch_size=self.batch_size, + ignore_conflicts=True, + ) + cve_to_alias_id.update( + AdvisoryAlias.objects.filter(alias__in=missing_cves).values_list("alias", "id") + ) + + # Mark previous days' scores as no longer latest ie ``is_latest=False`` + alias_ids = list(cve_to_alias_id.values()) + for i in range(0, len(alias_ids), self.batch_size): + alias_id_batch = alias_ids[i : i + self.batch_size] + EPSSScore.objects.filter( + alias_id__in=alias_id_batch, + date__lt=score_date, + is_latest=True, + ).update(is_latest=False) + + # Bulk insert today's scores as ``is_latest=True`` + scores = [] + for cve, (epss_score, percentile) in scores_by_cve.items(): + alias_id = cve_to_alias_id.get(cve) + if alias_id: + score = EPSSScore( + alias_id=alias_id, + date=score_date, + score=Decimal(epss_score), + percentile=Decimal(percentile), + is_latest=True, + ) + scores.append(score) + EPSSScore.objects.bulk_create( + scores, + batch_size=self.batch_size, + update_conflicts=True, + update_fields=["score", "percentile", "is_latest"], + unique_fields=["alias", "date"], + ) + + self.log(f"Successfully inserted {len(scores):,d} EPSS scores for {score_date}") + + def prune_old_scores(self): + """ + Delete EPSS score records older than ``retention_days``. + """ + cutoff = timezone.now().date() - timedelta(days=self.retention_days) + deleted_count, _ = EPSSScore.objects.filter(date__lt=cutoff).delete() + self.log( + f"Pruned {deleted_count:,d} EPSS records older than {cutoff} (retention: {self.retention_days} days)" + ) diff --git a/vulnerabilities/pipelines/v2_improvers/relate_severities.py b/vulnerabilities/pipelines/v2_improvers/relate_severities.py index 1fa8018cc..11216b1ef 100644 --- a/vulnerabilities/pipelines/v2_improvers/relate_severities.py +++ b/vulnerabilities/pipelines/v2_improvers/relate_severities.py @@ -15,7 +15,6 @@ from vulnerabilities.models import AdvisoryV2 from vulnerabilities.models import PipelineSchedule from vulnerabilities.pipelines import VulnerableCodePipeline -from vulnerabilities.pipelines.v2_importers.epss_importer_v2 import EPSSImporterPipeline from vulnerabilities.pipelines.v2_importers.suse_score_importer import ( SUSESeverityScoreImporterPipeline, ) @@ -23,14 +22,13 @@ from vulnerabilities.severity_systems import CVSSV3 from vulnerabilities.severity_systems import CVSSV4 from vulnerabilities.severity_systems import CVSSV31 -from vulnerabilities.severity_systems import EPSS logger = logging.getLogger(__name__) class RelateSeveritiesPipeline(VulnerableCodePipeline): """ - Severity Relations Pipeline: Relate EPSS and SUSE CVSS severities to advisories + Severity Relations Pipeline: Relate SUSE CVSS severities to advisories by matching severity advisory IDs with advisory IDs and aliases. """ @@ -42,7 +40,6 @@ class RelateSeveritiesPipeline(VulnerableCodePipeline): # Severity systems to process SUPPORTED_SYSTEMS = { - EPSS.identifier, CVSSV2.identifier, CVSSV3.identifier, CVSSV31.identifier, @@ -50,7 +47,6 @@ class RelateSeveritiesPipeline(VulnerableCodePipeline): } pipelines = [ - EPSSImporterPipeline.pipeline_id, SUSESeverityScoreImporterPipeline.pipeline_id, ] @@ -60,7 +56,7 @@ def steps(cls): def relate_severities(self): """ - Relate EPSS and SUSE severities to advisories by matching advisory IDs. + Relate SUSE severities to advisories by matching advisory IDs. """ # Filter severities by supported scoring systems severity_score_advisories = ( diff --git a/vulnerabilities/templates/advisory_detail.html b/vulnerabilities/templates/advisory_detail.html index fea5afde4..583fa52e2 100644 --- a/vulnerabilities/templates/advisory_detail.html +++ b/vulnerabilities/templates/advisory_detail.html @@ -505,17 +505,6 @@ {{ epss_data.source }} {% endif %} - {% if epss_data.advisory %} - - - - Advisory - - - {{ epss_data.advisory.avid }} - - {% endif %} {% else %} diff --git a/vulnerabilities/tests/pipelines/v2_importers/test_epss_importer_v2.py b/vulnerabilities/tests/pipelines/v2_importers/test_epss_importer_v2.py deleted file mode 100644 index bd463edf1..000000000 --- a/vulnerabilities/tests/pipelines/v2_importers/test_epss_importer_v2.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# Copyright (c) nexB Inc. and others. All rights reserved. -# VulnerableCode is a trademark of nexB Inc. -# SPDX-License-Identifier: Apache-2.0 -# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. -# See https://github.com/aboutcode-org/vulnerablecode for support or download. -# See https://aboutcode.org for more information about nexB OSS projects. -# - -from pathlib import Path - -import pytest - -from vulnerabilities.pipelines.v2_importers.epss_importer_v2 import EPSSImporterPipeline -from vulnerabilities.tests import util_tests - -TEST_DATA = Path(__file__).parent.parent.parent / "test_data" / "epss" - -TEST_CVE_FILES = (TEST_DATA / "epss_scores-2025-x-x.csv",) - - -@pytest.mark.django_db -@pytest.mark.parametrize("csv_file", TEST_CVE_FILES) -def test_epss_advisories_per_file(csv_file): - pipeline = EPSSImporterPipeline() - - with open(csv_file, "r") as f: - pipeline.lines = f.readlines() - - result = [adv.to_dict() for adv in pipeline.collect_advisories()] - expected_file = Path(TEST_DATA / "epss-expected.json") - util_tests.check_results_against_json(result, expected_file) diff --git a/vulnerabilities/tests/pipelines/v2_improvers/test_epss_pipeline.py b/vulnerabilities/tests/pipelines/v2_improvers/test_epss_pipeline.py new file mode 100644 index 000000000..99ebef47c --- /dev/null +++ b/vulnerabilities/tests/pipelines/v2_improvers/test_epss_pipeline.py @@ -0,0 +1,72 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/aboutcode-org/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +from datetime import timedelta +from decimal import Decimal +from pathlib import Path +from unittest import mock + +import pytest +from django.utils import timezone + +from vulnerabilities.models import AdvisoryAlias +from vulnerabilities.models import EPSSScore +from vulnerabilities.pipelines.v2_improvers.epss_pipeline import EPSSPipeline +from vulnerabilities.tests import util_tests + +TEST_DATA = Path(__file__).resolve().parent.parent.parent / "test_data" / "epss" + + +@pytest.mark.django_db +@mock.patch("urllib.request.urlopen") +def test_epss_pipeline(mock_urlopen): + test_file = TEST_DATA / "epss_scores_current.csv.gz" + with open(test_file, "rb") as f: + mock_urlopen.return_value = f + pipeline = EPSSPipeline() + pipeline.execute() + + results = [ + { + "cve": score.alias.alias, + "date": score.date.isoformat(), + "is_latest": score.is_latest, + "percentile": f"{score.percentile:.5f}", + "score": f"{score.score:.5f}", + } + for score in EPSSScore.objects.select_related("alias").order_by("alias__alias") + ] + expected_file = TEST_DATA / "epss_scores_expected.json" + util_tests.check_results_against_json(results, expected_file) + + +@pytest.mark.django_db +def test_epss_pipeline_prune_old_scores(): + alias = AdvisoryAlias.objects.create(alias="CVE-2021-25665") + today = timezone.now().date() + + recent = EPSSScore.objects.create( + alias=alias, + date=today - timedelta(days=30), + score=Decimal("0.00405"), + percentile=Decimal("0.61137"), + is_latest=True, + ) + old = EPSSScore.objects.create( + alias=alias, + date=today - timedelta(days=95), + score=Decimal("0.00300"), + percentile=Decimal("0.50000"), + is_latest=False, + ) + + EPSSPipeline().prune_old_scores() + + assert EPSSScore.objects.filter(id=recent.id).exists() + assert not EPSSScore.objects.filter(id=old.id).exists() diff --git a/vulnerabilities/tests/pipelines/v2_improvers/test_relate_severities.py b/vulnerabilities/tests/pipelines/v2_improvers/test_relate_severities.py index 04a730d70..4a2aebe9b 100644 --- a/vulnerabilities/tests/pipelines/v2_improvers/test_relate_severities.py +++ b/vulnerabilities/tests/pipelines/v2_improvers/test_relate_severities.py @@ -12,8 +12,11 @@ from vulnerabilities.models import AdvisorySeverity from vulnerabilities.models import AdvisoryV2 +from vulnerabilities.pipelines.v2_importers.suse_score_importer import ( + SUSESeverityScoreImporterPipeline, +) from vulnerabilities.pipelines.v2_improvers.relate_severities import RelateSeveritiesPipeline -from vulnerabilities.severity_systems import EPSS +from vulnerabilities.severity_systems import CVSSV3 @pytest.mark.django_db @@ -31,17 +34,17 @@ def test_relate_severities_by_advisory_id(): severity_advisory = AdvisoryV2.objects.create( advisory_id="CVE-2024-0001", - datasource_id="epss", - avid="epss/CVE-2024-0001", + datasource_id="suse", + avid="suse/CVE-2024-0001", unique_content_id="ab2", - url="https://example.com/epss/CVE-2024-0001", + url="https://example.com/suse/CVE-2024-0001", date_collected="2024-01-02", is_latest=True, - pipeline_id="epss_importer_v2", + pipeline_id=SUSESeverityScoreImporterPipeline.pipeline_id, ) severity_advisory.severities.create( - scoring_system=EPSS.identifier, - value="0.5", + scoring_system=CVSSV3.identifier, + value="5.5", ) pipeline = RelateSeveritiesPipeline() @@ -71,17 +74,17 @@ def test_relate_severities_via_alias(): severity_advisory = AdvisoryV2.objects.create( advisory_id="CVE-2024-ALIAS", - datasource_id="epss", - avid="epss/CVE-2024-ALIAS", + datasource_id="suse", + avid="suse/CVE-2024-ALIAS", unique_content_id="ab4", - url="https://example.com/epss/CVE-2024-ALIAS", + url="https://example.com/suse/CVE-2024-ALIAS", date_collected="2024-01-02", is_latest=True, - pipeline_id="epss_importer_v2", + pipeline_id=SUSESeverityScoreImporterPipeline.pipeline_id, ) severity_advisory.severities.create( - scoring_system=EPSS.identifier, - value="0.8", + scoring_system=CVSSV3.identifier, + value="8.8", ) pipeline = RelateSeveritiesPipeline() @@ -94,17 +97,17 @@ def test_relate_severities_via_alias(): def test_no_self_relation_created(): advisory = AdvisoryV2.objects.create( advisory_id="CVE-2024-0003", - datasource_id="epss", + datasource_id="suse", unique_content_id="ab5", url="https://example.com/advisory/CVE-2024-0003", date_collected="2024-01-03", - avid="epss/CVE-2024-0003", + avid="suse/CVE-2024-0003", is_latest=True, - pipeline_id="epss_importer_v2", + pipeline_id=SUSESeverityScoreImporterPipeline.pipeline_id, ) advisory.severities.create( - scoring_system=EPSS.identifier, - value="0.2", + scoring_system=CVSSV3.identifier, + value="2.2", ) pipeline = RelateSeveritiesPipeline() @@ -128,13 +131,13 @@ def test_unsupported_severity_system_is_ignored(): severity_advisory = AdvisoryV2.objects.create( advisory_id="CVE-2024-0004", - datasource_id="epss", + datasource_id="suse", unique_content_id="ab7", - url="https://example.com/epss/CVE-2024-0004", + url="https://example.com/suse/CVE-2024-0004", date_collected="2024-01-02", - avid="epss/CVE-2024-0004", + avid="suse/CVE-2024-0004", is_latest=True, - pipeline_id="epss_importer_v2", + pipeline_id=SUSESeverityScoreImporterPipeline.pipeline_id, ) severity_advisory.severities.create( scoring_system="UNKNOWN_SYSTEM", @@ -162,17 +165,17 @@ def test_pipeline_is_idempotent(): severity = AdvisoryV2.objects.create( advisory_id="CVE-2024-0005", - datasource_id="epss", + datasource_id="suse", unique_content_id="ab9", - url="https://example.com/epss/CVE-2024-0005", + url="https://example.com/suse/CVE-2024-0005", date_collected="2024-01-02", is_latest=True, - avid="epss/CVE-2024-0005", - pipeline_id="epss_importer_v2", + avid="suse/CVE-2024-0005", + pipeline_id=SUSESeverityScoreImporterPipeline.pipeline_id, ) severity.severities.create( - scoring_system=EPSS.identifier, - value="0.9", + scoring_system=CVSSV3.identifier, + value="9.0", ) pipeline = RelateSeveritiesPipeline() diff --git a/vulnerabilities/tests/test_data/epss/epss-expected.json b/vulnerabilities/tests/test_data/epss/epss-expected.json deleted file mode 100644 index a690e59ad..000000000 --- a/vulnerabilities/tests/test_data/epss/epss-expected.json +++ /dev/null @@ -1,677 +0,0 @@ -[ - { - "advisory_id": "CVE-1999-0001", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-1999-0001" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.01151", - "scoring_elements": "0.7802", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-1999-0002", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-1999-0002" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.10546", - "scoring_elements": "0.93015", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-1999-0003", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-1999-0003" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.89352", - "scoring_elements": "0.99521", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-1999-0004", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-1999-0004" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.03037", - "scoring_elements": "0.86249", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9970", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9970" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "7e-05", - "scoring_elements": "0.00428", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9971", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9971" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.0024", - "scoring_elements": "0.47036", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9972", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9972" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00543", - "scoring_elements": "0.67059", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9975", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9975" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00039", - "scoring_elements": "0.11657", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9976", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9976" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00085", - "scoring_elements": "0.25184", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9977", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9977" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.06195", - "scoring_elements": "0.90539", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9978", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9978" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00045", - "scoring_elements": "0.13792", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9979", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9979" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00037", - "scoring_elements": "0.10925", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9980", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9980" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00031", - "scoring_elements": "0.08656", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9981", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9981" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00031", - "scoring_elements": "0.08656", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9982", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9982" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00053", - "scoring_elements": "0.16926", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9983", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9983" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00023", - "scoring_elements": "0.05543", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9984", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9984" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00055", - "scoring_elements": "0.17352", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9985", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9985" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.01936", - "scoring_elements": "0.82968", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9990", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9990" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00343", - "scoring_elements": "0.56418", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9991", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9991" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00343", - "scoring_elements": "0.56418", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9992", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9992" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00043", - "scoring_elements": "0.13241", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9993", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9993" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00129", - "scoring_elements": "0.33233", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9994", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9994" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00114", - "scoring_elements": "0.30855", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9996", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9996" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00241", - "scoring_elements": "0.47334", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9997", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9997" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00245", - "scoring_elements": "0.47699", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9998", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9998" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00022", - "scoring_elements": "0.05357", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - }, - { - "advisory_id": "CVE-2025-9999", - "aliases": [], - "summary": "", - "affected_packages": [], - "references": [ - { - "reference_id": "", - "reference_type": "", - "url": "https://api.first.org/data/v1/epss?cve=CVE-2025-9999" - } - ], - "patches": [], - "severities": [ - { - "system": "epss", - "value": "0.00044", - "scoring_elements": "0.13444", - "published_at": "2025-12-26T12:55:00+00:00" - } - ], - "date_published": null, - "weaknesses": [], - "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" - } -] \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/epss/epss_scores-2025-x-x.csv b/vulnerabilities/tests/test_data/epss/epss_scores-2025-x-x.csv deleted file mode 100644 index fd2a04010..000000000 --- a/vulnerabilities/tests/test_data/epss/epss_scores-2025-x-x.csv +++ /dev/null @@ -1,29 +0,0 @@ -#model_version:v2025.03.14,score_date:2025-12-26T12:55:00Z -cve,epss,percentile -CVE-1999-0001,0.01151,0.7802 -CVE-1999-0002,0.10546,0.93015 -CVE-1999-0003,0.89352,0.99521 -CVE-1999-0004,0.03037,0.86249 -CVE-2025-9970,7e-05,0.00428 -CVE-2025-9971,0.0024,0.47036 -CVE-2025-9972,0.00543,0.67059 -CVE-2025-9975,0.00039,0.11657 -CVE-2025-9976,0.00085,0.25184 -CVE-2025-9977,0.06195,0.90539 -CVE-2025-9978,0.00045,0.13792 -CVE-2025-9979,0.00037,0.10925 -CVE-2025-9980,0.00031,0.08656 -CVE-2025-9981,0.00031,0.08656 -CVE-2025-9982,0.00053,0.16926 -CVE-2025-9983,0.00023,0.05543 -CVE-2025-9984,0.00055,0.17352 -CVE-2025-9985,0.01936,0.82968 -CVE-2025-9990,0.00343,0.56418 -CVE-2025-9991,0.00343,0.56418 -CVE-2025-9992,0.00043,0.13241 -CVE-2025-9993,0.00129,0.33233 -CVE-2025-9994,0.00114,0.30855 -CVE-2025-9996,0.00241,0.47334 -CVE-2025-9997,0.00245,0.47699 -CVE-2025-9998,0.00022,0.05357 -CVE-2025-9999,0.00044,0.13444 \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/epss/epss_scores_current.csv.gz b/vulnerabilities/tests/test_data/epss/epss_scores_current.csv.gz new file mode 100644 index 000000000..e8882045c Binary files /dev/null and b/vulnerabilities/tests/test_data/epss/epss_scores_current.csv.gz differ diff --git a/vulnerabilities/tests/test_data/epss/epss_scores_expected.json b/vulnerabilities/tests/test_data/epss/epss_scores_expected.json new file mode 100644 index 000000000..fac5e118e --- /dev/null +++ b/vulnerabilities/tests/test_data/epss/epss_scores_expected.json @@ -0,0 +1,79 @@ +[ + { + "cve": "CVE-2021-25665", + "date": "2026-10-28", + "is_latest": true, + "percentile": "0.61137", + "score": "0.00405" + }, + { + "cve": "CVE-2021-25666", + "date": "2026-10-28", + "is_latest": true, + "percentile": "0.21821", + "score": "0.00072" + }, + { + "cve": "CVE-2021-25667", + "date": "2026-10-28", + "is_latest": true, + "percentile": "0.53835", + "score": "0.00305" + }, + { + "cve": "CVE-2021-25668", + "date": "2026-10-28", + "is_latest": true, + "percentile": "0.79860", + "score": "0.01284" + }, + { + "cve": "CVE-2021-25669", + "date": "2026-10-28", + "is_latest": true, + "percentile": "0.82606", + "score": "0.01715" + }, + { + "cve": "CVE-2021-25670", + "date": "2026-10-28", + "is_latest": true, + "percentile": "0.58798", + "score": "0.00367" + }, + { + "cve": "CVE-2021-25671", + "date": "2026-10-28", + "is_latest": true, + "percentile": "0.27770", + "score": "0.00104" + }, + { + "cve": "CVE-2021-25672", + "date": "2026-10-28", + "is_latest": true, + "percentile": "0.56506", + "score": "0.00336" + }, + { + "cve": "CVE-2021-25673", + "date": "2026-10-28", + "is_latest": true, + "percentile": "0.12359", + "score": "0.00040" + }, + { + "cve": "CVE-2021-25674", + "date": "2026-10-28", + "is_latest": true, + "percentile": "0.13878", + "score": "0.00045" + }, + { + "cve": "CVE-2021-25675", + "date": "2026-10-28", + "is_latest": true, + "percentile": "0.13878", + "score": "0.00045" + } +] diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index b5ba74214..3acb807c3 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -56,7 +56,6 @@ from vulnerabilities.models import ImpactedPackageFixedBy from vulnerabilities.models import PipelineRun from vulnerabilities.models import PipelineSchedule -from vulnerabilities.pipelines.v2_importers.epss_importer_v2 import EPSSImporterPipeline from vulnerabilities.severity_systems import EPSS from vulnerabilities.severity_systems import SCORING_SYSTEMS from vulnerabilities.tasks import compute_queue_load_factor @@ -582,6 +581,35 @@ def get_context_data(self, **kwargs): return context +def get_epss_data_for_advisory(advisory): + """ + Return the latest EPSS score data mapping for the given ``advisory``, + or None if no score is found. + """ + cve_id = advisory.advisory_id if advisory.advisory_id.startswith("CVE-") else None + + # Resolve latest EPSS score via linked AdvisoryAlias records or directly by ``advisory_id`` (when advisory_id is itself a CVE) + epss_filter = Q(alias__advisories=advisory) + if cve_id: + epss_filter |= Q(alias__alias=cve_id) + + latest_epss = ( + models.EPSSScore.objects.filter(epss_filter, is_latest=True) + .select_related("alias") + .order_by("-score") # Surface highest EPSS score if advisory links to multiple CVEs + .first() + ) + if not latest_epss: + return None + + return { + "percentile": f"{latest_epss.percentile:.5f}", + "score": f"{latest_epss.score:.5f}", + "published_at": latest_epss.date, + "source": f"https://api.first.org/data/v1/epss?cve={latest_epss.alias.alias}", + } + + class AdvisoryDetails(VulnerableCodeDetailView): model = models.AdvisoryV2 template_name = "advisory_detail.html" @@ -672,28 +700,7 @@ def get_context_data(self, **kwargs): .exclude(scoring_elements="") ) - epss_severity = advisory.severities.filter(scoring_system="epss").first() - epss_data = None - epss_advisory = None - if not epss_severity: - epss_advisory = ( - advisory.related_advisory_severities.filter( - datasource_id=EPSSImporterPipeline.pipeline_id - ) - .latest_per_avid() - .first() - ) - if epss_advisory: - epss_severity = epss_advisory.severities.filter(scoring_system="epss").first() - if epss_severity: - # If the advisory itself does not have EPSS severity, but has a related advisory with EPSS severity, we use the related advisory's EPSS severity and URL as the source of EPSS data. - epss_data = { - "percentile": epss_severity.scoring_elements, - "score": epss_severity.value, - "published_at": epss_severity.published_at, - "source": epss_advisory.url if epss_advisory else advisory.url, - "advisory": epss_advisory if epss_advisory else advisory, - } + epss_data = get_epss_data_for_advisory(advisory) ssvc_entries = [] seen = set()