diff --git a/.gitignore b/.gitignore index 63fb1b5b0..f80aa52b5 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,7 @@ Pipfile # editors .vscode +*.code-workspace # PyCharm .idea/ diff --git a/vulnerabilities/templates/advisory_detail.html b/vulnerabilities/templates/advisory_detail.html index 0b850c006..6905d30cb 100644 --- a/vulnerabilities/templates/advisory_detail.html +++ b/vulnerabilities/templates/advisory_detail.html @@ -25,7 +25,7 @@
-
+
@@ -438,7 +438,7 @@ -
+
{% if epss_data %}
Exploit Prediction Scoring System (EPSS) @@ -498,6 +498,68 @@ {% endif %}
+ + {% if epss_history_data|length > 1 %} +
+ EPSS Score History +
+ +
+ + +
+ + + +
+ {% endif %} {% else %}

No EPSS data available for this advisory.

{% endif %} @@ -694,6 +756,11 @@ + + + +{{ epss_history_data|json_script:"epss-history-data" }} + + + {% endblock %} \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/epss/epss_history_expected.json b/vulnerabilities/tests/test_data/epss/epss_history_expected.json new file mode 100644 index 000000000..dbdce2699 --- /dev/null +++ b/vulnerabilities/tests/test_data/epss/epss_history_expected.json @@ -0,0 +1,152 @@ +[ + { + "score": 0.00048, + "percentile": 0.14616, + "published_at": "2026-05-02" + }, + { + "score": 0.00048, + "percentile": 0.14593, + "published_at": "2026-05-03" + }, + { + "score": 0.00048, + "percentile": 0.1453, + "published_at": "2026-05-04" + }, + { + "score": 0.00048, + "percentile": 0.1453, + "published_at": "2026-05-05" + }, + { + "score": 0.00048, + "percentile": 0.1453, + "published_at": "2026-05-06" + }, + { + "score": 0.00048, + "percentile": 0.14663, + "published_at": "2026-05-07" + }, + { + "score": 0.00048, + "percentile": 0.14697, + "published_at": "2026-05-08" + }, + { + "score": 0.00048, + "percentile": 0.14752, + "published_at": "2026-05-09" + }, + { + "score": 0.00048, + "percentile": 0.14765, + "published_at": "2026-05-10" + }, + { + "score": 0.00048, + "percentile": 0.14748, + "published_at": "2026-05-11" + }, + { + "score": 0.00048, + "percentile": 0.14793, + "published_at": "2026-05-12" + }, + { + "score": 0.00048, + "percentile": 0.14815, + "published_at": "2026-05-13" + }, + { + "score": 0.00048, + "percentile": 0.14874, + "published_at": "2026-05-14" + }, + { + "score": 0.00048, + "percentile": 0.14881, + "published_at": "2026-05-15" + }, + { + "score": 0.00048, + "percentile": 0.14905, + "published_at": "2026-05-16" + }, + { + "score": 0.00048, + "percentile": 0.14886, + "published_at": "2026-05-17" + }, + { + "score": 0.00048, + "percentile": 0.14837, + "published_at": "2026-05-18" + }, + { + "score": 0.00048, + "percentile": 0.14816, + "published_at": "2026-05-19" + }, + { + "score": 0.00048, + "percentile": 0.14827, + "published_at": "2026-05-20" + }, + { + "score": 0.00048, + "percentile": 0.14803, + "published_at": "2026-05-21" + }, + { + "score": 0.00048, + "percentile": 0.14978, + "published_at": "2026-05-22" + }, + { + "score": 0.00048, + "percentile": 0.14966, + "published_at": "2026-05-23" + }, + { + "score": 0.00048, + "percentile": 0.14918, + "published_at": "2026-05-24" + }, + { + "score": 0.00048, + "percentile": 0.14902, + "published_at": "2026-05-25" + }, + { + "score": 0.00048, + "percentile": 0.14898, + "published_at": "2026-05-26" + }, + { + "score": 0.00048, + "percentile": 0.14998, + "published_at": "2026-05-27" + }, + { + "score": 0.00048, + "percentile": 0.15143, + "published_at": "2026-05-28" + }, + { + "score": 0.00048, + "percentile": 0.15216, + "published_at": "2026-05-29" + }, + { + "score": 0.00048, + "percentile": 0.15204, + "published_at": "2026-05-30" + }, + { + "score": 0.00048, + "percentile": 0.15171, + "published_at": "2026-05-31" + } +] \ No newline at end of file diff --git a/vulnerabilities/tests/test_data/epss/epss_history_test_data.json b/vulnerabilities/tests/test_data/epss/epss_history_test_data.json new file mode 100644 index 000000000..6a1b4d62c --- /dev/null +++ b/vulnerabilities/tests/test_data/epss/epss_history_test_data.json @@ -0,0 +1,777 @@ +[ + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.000480000", + "scoring_elements": "0.151710000", + "published_at": "2026-05-31T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.000480000", + "scoring_elements": "0.152040000", + "published_at": "2026-05-30T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.15216000", + "published_at": "2026-05-29T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.15143000", + "published_at": "2026-05-28T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14998000", + "published_at": "2026-05-27T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14898000", + "published_at": "2026-05-26T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14902000", + "published_at": "2026-05-25T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14918000", + "published_at": "2026-05-24T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14966000", + "published_at": "2026-05-23T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14978000", + "published_at": "2026-05-22T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14803000", + "published_at": "2026-05-21T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14827000", + "published_at": "2026-05-20T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14816000", + "published_at": "2026-05-19T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14837000", + "published_at": "2026-05-18T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14886000", + "published_at": "2026-05-17T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14905000", + "published_at": "2026-05-16T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14881000", + "published_at": "2026-05-15T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14874000", + "published_at": "2026-05-14T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14815000", + "published_at": "2026-05-13T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14793000", + "published_at": "2026-05-12T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14748000", + "published_at": "2026-05-11T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14765000", + "published_at": "2026-05-10T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14752000", + "published_at": "2026-05-09T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14697000", + "published_at": "2026-05-08T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14663000", + "published_at": "2026-05-07T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14530000", + "published_at": "2026-05-06T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14530000", + "published_at": "2026-05-05T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14530000", + "published_at": "2026-05-04T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14593000", + "published_at": "2026-05-03T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14616000", + "published_at": "2026-05-02T00:00:00+00:00" + } + ], + "date_published": null, + "weaknesses": [], + "url": "https://epss.cyentia.com/epss_scores-current.csv.gz" + }, + { + "advisory_id": "CVE-2022-25204", + "aliases": [], + "summary": "", + "affected_packages": [], + "references": [ + { + "reference_id": "", + "reference_type": "", + "url": "https://api.first.org/data/v1/epss?cve=CVE-2022-25204" + } + ], + "patches": [], + "severities": [ + { + "system": "epss", + "value": "0.00048000", + "scoring_elements": "0.14603000", + "published_at": "2026-05-01T00:00: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_epss_history.py b/vulnerabilities/tests/test_epss_history.py new file mode 100644 index 000000000..e3658b051 --- /dev/null +++ b/vulnerabilities/tests/test_epss_history.py @@ -0,0 +1,58 @@ +import json +from pathlib import Path + +from django.test import TestCase +from django.urls import reverse + +from vulnerabilities.models import AdvisoryAlias +from vulnerabilities.models import AdvisoryReference +from vulnerabilities.models import AdvisorySeverity +from vulnerabilities.models import AdvisoryV2 +from vulnerabilities.tests.util_tests import check_results_against_json + + +class AdvisoryDetailsEpssTestCase(TestCase): + def setUp(self): + json_file = Path(__file__).parent / "test_data" / "epss" / "epss_history_test_data.json" + with open(json_file, "r") as f: + json_data = json.load(f) + + ref_obj = AdvisoryReference.objects.create(url=json_data[0]["references"][0]["url"]) + + for i, data in enumerate(json_data): + is_latest = i == 0 + + advisory = AdvisoryV2.objects.create( + avid=f"test_epss/advisory-{i}", + advisory_id="CVE-2022-25204", + url="https://epss.cyentia.com/epss_scores-current.csv.gz", + is_latest=is_latest, + datasource_id="test_epss", + pipeline_id="test_epss", + unique_content_id=f"hash_{i}", + ) + + advisory.references.add(ref_obj) + + severity = data["severities"][0] + advisory.severities.create( + scoring_system=severity["system"], + value=severity["value"], + scoring_elements=severity["scoring_elements"], + published_at=severity["published_at"], + ) + + if i == 0: + self.advisory = advisory + + def test_epss_history_context(self): + response = self.client.get(reverse("advisory_details", kwargs={"avid": self.advisory.avid})) + self.assertEqual(response.status_code, 200) + + expected_file = Path(__file__).parent / "test_data" / "epss" / "epss_history_expected.json" + history_data_for_json = [ + {**record, "published_at": record["published_at"].isoformat()} + for record in response.context["epss_history_data"] + ] + + check_results_against_json(history_data_for_json, expected_file) diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 04ac8a787..88fab44a9 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -20,10 +20,17 @@ from django.core.cache import cache from django.core.exceptions import ValidationError from django.core.mail import send_mail +from django.core.paginator import Paginator from django.db.models import Exists +from django.db.models import FloatField +from django.db.models import Max from django.db.models import OuterRef from django.db.models import Prefetch +from django.db.models.functions import Cast +from django.db.models.functions import TruncDate +from django.http import HttpRequest from django.http import HttpResponse +from django.http import JsonResponse from django.http.response import Http404 from django.shortcuts import get_object_or_404 from django.shortcuts import redirect @@ -31,6 +38,7 @@ from django.urls import reverse_lazy from django.views import View from django.views import generic +from django.views.decorators.http import require_safe from django.views.generic.detail import DetailView from django.views.generic.edit import FormMixin from django.views.generic.list import ListView @@ -751,6 +759,49 @@ def add_ssvc(ssvc): add_ssvc(ssvc) context["ssvcs"] = ssvc_entries + + # EPSS history + cves = { + alias_obj.alias + for alias_obj in advisory.aliases.all() + if alias_obj.alias.startswith("CVE-") + } + if advisory.advisory_id and advisory.advisory_id.startswith("CVE-"): + cves.add(advisory.advisory_id) + + if cves: + epss_scores_queryset = ( + models.AdvisorySeverity.objects.filter( + advisories__advisory_id__in=cves, + scoring_system=EPSS.identifier, + published_at__isnull=False, + ) + .annotate(pub_date=TruncDate("published_at")) + .values("pub_date") + .annotate( + max_score=Max(Cast("value", FloatField())), + max_percentile=Max(Cast("scoring_elements", FloatField())), + ) + .order_by("-pub_date") + ) + + paginator = Paginator(epss_scores_queryset, 30) + epss_page = self.request.GET.get("epss_page", 1) + epss_page_obj = paginator.get_page(epss_page) + + epss_history_data = [ + { + "score": record["max_score"], + "percentile": record["max_percentile"], + "published_at": record["pub_date"], + } + for record in epss_page_obj.object_list + ] + epss_history_data.reverse() + else: + epss_history_data = [] + epss_page_obj = None + context.update( { "advisory": advisory, @@ -761,6 +812,9 @@ def add_ssvc(ssvc): "weaknesses": weaknesses_present_in_db, "status": advisory.get_status_label, "epss_data": epss_data, + "epss_history_data": epss_history_data, + "epss_page_obj": epss_page_obj, + "is_epss_tab_active": "epss_page" in self.request.GET, } ) return context diff --git a/vulnerablecode/static/css/custom.css b/vulnerablecode/static/css/custom.css index 6d8918a8f..1209f0588 100644 --- a/vulnerablecode/static/css/custom.css +++ b/vulnerablecode/static/css/custom.css @@ -655,3 +655,13 @@ ul.fixed_by_bullet li li li { box-shadow: none; } } + +/* Tooltip styles for EPSS charts */ +.bb-tooltip-container { + min-width: 220px !important; +} + +.bb-tooltip th, +.bb-tooltip td { + white-space: nowrap !important; +} diff --git a/vulnerablecode/static/js/advisory_detail.js b/vulnerablecode/static/js/advisory_detail.js new file mode 100644 index 000000000..6e4cdbab0 --- /dev/null +++ b/vulnerablecode/static/js/advisory_detail.js @@ -0,0 +1,66 @@ +(function () { + let epssChartInstance = null; + + const [btnChart, btnTable, chartWrap, tableWrap] = + ["btn-load-epss-chart", "btn-load-epss-table", "epss-chart-wrap", "epss-history-table-wrap"] + .map(id => document.getElementById(id)); + + const getHistoryData = () => JSON.parse(document.getElementById('epss-history-data')?.textContent || "[]"); + const toggleDisplay = (el) => el.style.display = el.style.display === "none" ? "block" : "none"; + + function renderChart() { + const data = getHistoryData(); + if (!data.length) return; + + toggleDisplay(chartWrap); + if (chartWrap.style.display === "none" || epssChartInstance) return; + + const history = []; + const map = new Map(data.map(h => [new Date(h.published_at + "T00:00:00").setHours(0,0,0,0), h])); + + const end = new Date(data[data.length - 1].published_at + "T00:00:00").setHours(0,0,0,0); + let start = new Date(end); + start.setDate(start.getDate() - 30); + + const actualStart = new Date(data[0].published_at + "T00:00:00").setHours(0,0,0,0); + start = new Date(Math.max(start.getTime(), actualStart)); + + for (let d = start; d.getTime() <= end; d.setDate(d.getDate() + 1)) { + history.push(map.get(d.getTime()) || { + published_at: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`, + score: null, percentile: null + }); + } + try { + epssChartInstance = bb.generate({ + bindto: "#epss-chart", + size: { height: 260 }, padding: { right: 25 }, + data: { + x: "Date", xFormat: "%Y-%m-%d", + columns: [ + ["Date", ...history.map(h => h.published_at || "")], + ["Score", ...history.map(h => h.score === null ? null : parseFloat(h.score))], + ["Percentile", ...history.map(h => h.percentile === null ? null : parseFloat(h.percentile))], + ], + type: "line", colors: { Score: "#df25e6ff", Percentile: "#00d1b2" }, + }, + //Handles missing dates + line: { connectNull: false }, + axis: { + x: { type: "timeseries", tick: { format: "%b %d", count: Math.min(history.length, 6), fit: true } }, + y: { min: 0, max: 1, padding: { top: 10, bottom: 0 }, tick: { format: v => v.toFixed(4) } }, + }, + point: { r: 3 }, legend: { show: true }, + tooltip: { + format: { + title: d => d instanceof Date ? `${d.toLocaleString('en-us', {month: 'short'})} ${String(d.getDate()).padStart(2, '0')}, ${d.getFullYear()}` : String(d), + value: v => v.toFixed(5), + }, + }, + }); + } catch (e) { console.error("[epss-chart]", e); } + } + + btnChart?.addEventListener("click", renderChart); + btnTable?.addEventListener("click", () => toggleDisplay(tableWrap)); +})();