Repository: pypi/warehouse

Issue:
Vulnerability reporting task is not resilient to deleted releases
Currently attempting to publish a vulnerability report for a notification that includes release versions that have been deleted (or never existed) causes an error and prevents the entire report from being stored.

Example: https://sentry.io/share/issue/69362937025d4524be01918a80f830d1/

If the release is not found, we should just skip that version and continue storing the vulnerability report. If *none* of the versions are found, we should error.


Discussion:
Hi @di, seems like no one is working on this issue. I'm interested, can you tell me what do I have to do?
Sure. The `analyze_vulnerability` function handles a report of a vulnerability for multiple release versions. Currently it fails if one of the releases are missing:

https://github.com/pypa/warehouse/blob/b0da6add8783535879057d011ee5300330a154f8/warehouse/integrations/vulnerabilities/utils.py#L105-L112

(This might happen due to a typo, or more likely if the release has been removed since the vulnerability has been discovered)

If one or more of the versions in the report don't correspond to a release, we should just skip that version and continue storing the vulnerability report. If none of the versions in the report are found, we should error.
Thanks. I'll work on it later.

Resolving patch:
diff --git a/warehouse/integrations/vulnerabilities/utils.py b/warehouse/integrations/vulnerabilities/utils.py
--- a/warehouse/integrations/vulnerabilities/utils.py
+++ b/warehouse/integrations/vulnerabilities/utils.py
@@ -10,7 +10,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
+from pyramid.httpexceptions import HTTPBadRequest
 from sqlalchemy import func, orm
 from sqlalchemy.orm.exc import NoResultFound
 
@@ -101,19 +101,26 @@ def _analyze_vulnerability(request, vulnerability_report, origin, metrics):
         )
         raise
 
+    found_releases = False  # by now, we don't have any release found
+
     for version in report.versions:
         try:
             release = _get_release(request, project, version)
+            found_releases = True  # at least one release found
         except NoResultFound:
             metrics.increment(
                 "warehouse.vulnerabilities.error.release_not_found",
                 tags=[f"origin:{origin}"],
             )
-            raise
+            continue  # skip that release
 
         if release not in vulnerability_record.releases:
             vulnerability_record.releases.append(release)
 
+    if not found_releases:
+        # no releases found, then raise an exception
+        raise HTTPBadRequest("None of the releases were found")
+
     # Unassociate any releases that no longer apply.
     for release in list(vulnerability_record.releases):
         if release.version not in report.versions:
@@ -136,7 +143,11 @@ def analyze_vulnerability(request, vulnerability_report, origin, metrics):
         metrics.increment(
             "warehouse.vulnerabilities.processed", tags=[f"origin:{origin}"]
         )
-    except (vulnerabilities.InvalidVulnerabilityReportRequest, NoResultFound):
+    except (
+        vulnerabilities.InvalidVulnerabilityReportRequest,
+        NoResultFound,
+        HTTPBadRequest,
+    ):
         raise
     except Exception:
         metrics.increment(


Test patch:
diff --git a/tests/unit/integration/vulnerabilities/test_utils.py b/tests/unit/integration/vulnerabilities/test_utils.py
--- a/tests/unit/integration/vulnerabilities/test_utils.py
+++ b/tests/unit/integration/vulnerabilities/test_utils.py
@@ -16,6 +16,7 @@
 import pretend
 import pytest
 
+from pyramid.httpexceptions import HTTPBadRequest
 from sqlalchemy.orm.exc import NoResultFound
 
 from tests.common.db.packaging import ProjectFactory, ReleaseFactory
@@ -289,7 +290,7 @@ def metrics_increment(key, tags):
 
     metrics = pretend.stub(increment=metrics_increment, timed=metrics.timed)
 
-    with pytest.raises(NoResultFound):
+    with pytest.raises(HTTPBadRequest):
         utils.analyze_vulnerability(
             request=db_request,
             vulnerability_report={
@@ -309,7 +310,7 @@ def metrics_increment(key, tags):
         (
             "warehouse.vulnerabilities.error.release_not_found",
             ("origin:test_report_source",),
-        ): 1,
+        ): 2,
     }