From 5cb5632fb33f46024d87e2ff2cf097204acf7391 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Thu, 6 Aug 2026 22:28:44 +0530 Subject: [PATCH 1/6] feat: include advisory todo count in api response Signed-off-by: Keshav Priyadarshi --- vulnerabilities/api_v3.py | 143 ++++++++++++++++++++-------------- vulnerabilities/throttling.py | 2 +- 2 files changed, 85 insertions(+), 60 deletions(-) diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index 83c5cf8bf..36a841cce 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -12,6 +12,7 @@ from django.contrib.postgres.aggregates import ArrayAgg from django.core.cache import cache +from django.db.models import Count from django.db.models import Exists from django.db.models import F from django.db.models import Max @@ -126,6 +127,7 @@ class AdvisoryV3Serializer(serializers.ModelSerializer): severities = AdvisorySeveritySerializer(many=True) advisory_uid = serializers.CharField(source="avid", read_only=True) related_ssvc_trees = serializers.SerializerMethodField() + todo_count = serializers.IntegerField(read_only=True) def get_related_ssvc_trees(self, obj): seen = set() @@ -165,6 +167,7 @@ class Meta: "weighted_severity", "risk_score", "related_ssvc_trees", + "todo_count", ] @@ -396,51 +399,58 @@ def create(self, request, *args, **kwargs): purls = serializer.validated_data["purls"] - latest_advisories = AdvisoryV2.objects.latest_advisories_for_purls( - purls=purls - ).prefetch_related( - Prefetch( - "references", - queryset=AdvisoryReference.objects.only( - "id", - "url", - "reference_type", - "reference_id", + latest_advisories = ( + AdvisoryV2.objects.latest_advisories_for_purls(purls=purls) + .annotate( + todo_count=Count( + "advisory_todos", + filter=Q(advisory_todos__is_todo_stale=False), + ) + ) + .prefetch_related( + Prefetch( + "references", + queryset=AdvisoryReference.objects.only( + "id", + "url", + "reference_type", + "reference_id", + ), ), - ), - Prefetch( - "severities", - queryset=AdvisorySeverity.objects.only( - "id", - "url", - "value", - "scoring_system", - "scoring_elements", - "published_at", + Prefetch( + "severities", + queryset=AdvisorySeverity.objects.only( + "id", + "url", + "value", + "scoring_system", + "scoring_elements", + "published_at", + ), ), - ), - "weaknesses", - "aliases", - Prefetch( - "related_ssvcs", - queryset=SSVC.objects.only( - "id", - "vector", - "decision", - "options", - "source_advisory__url", + "weaknesses", + "aliases", + Prefetch( + "related_ssvcs", + queryset=SSVC.objects.only( + "id", + "vector", + "decision", + "options", + "source_advisory__url", + ), ), - ), - Prefetch( - "source_ssvcs", - queryset=SSVC.objects.only( - "id", - "vector", - "decision", - "options", - "source_advisory__url", + Prefetch( + "source_ssvcs", + queryset=SSVC.objects.only( + "id", + "vector", + "decision", + "options", + "source_advisory__url", + ), ), - ), + ) ) page = self.paginate_queryset(latest_advisories) @@ -627,6 +637,10 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit max_exploitability=Max( "members__advisory__exploitability", ), + todo_count=Count( + "primary_advisory__advisory_todos", + filter=Q(primary_advisory__advisory_todos__is_todo_stale=False), + ), ) .only( "id", @@ -772,6 +786,7 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit ), "ssvc_trees": adv.ssvc_trees, "resource_url": resource_url, + "todo_count": adv.todo_count, } ) @@ -809,25 +824,34 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit if not allowed_package_ids: return result - advisories = AdvisoryV2.objects.filter( - id__in=allowed_advisory_ids, - ).prefetch_related( - "aliases", - Prefetch( - "related_ssvcs", - queryset=( - SSVC.objects.select_related("source_advisory") - .only( - "id", - "decision", - "options", - "vector", - "source_advisory__url", - ) - .distinct("source_advisory__url") + advisories = ( + AdvisoryV2.objects.filter( + id__in=allowed_advisory_ids, + ) + .annotate( + todo_count=Count( + "advisory_todos", + filter=Q(advisory_todos__is_todo_stale=False), + ) + ) + .prefetch_related( + "aliases", + Prefetch( + "related_ssvcs", + queryset=( + SSVC.objects.select_related("source_advisory") + .only( + "id", + "decision", + "options", + "vector", + "source_advisory__url", + ) + .distinct("source_advisory__url") + ), + to_attr="prefetched_ssvc_trees", ), - to_attr="prefetched_ssvc_trees", - ), + ) ) advisory_by_id = {advisory.id: advisory for advisory in advisories} @@ -880,6 +904,7 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit for ssvc in advisory.prefetched_ssvc_trees ], "resource_url": resource_url, + "todo_count": advisory.todo_count, } ) diff --git a/vulnerabilities/throttling.py b/vulnerabilities/throttling.py index c97b2c89f..4ff5f6b88 100644 --- a/vulnerabilities/throttling.py +++ b/vulnerabilities/throttling.py @@ -10,7 +10,6 @@ from django.core.exceptions import ImproperlyConfigured from rest_framework.exceptions import Throttled from rest_framework.throttling import UserRateThrottle -from rest_framework.views import exception_handler class PermissionBasedUserRateThrottle(UserRateThrottle): @@ -68,6 +67,7 @@ def throttled_exception_handler(exception, context): """ Return this response whenever a request has been throttled """ + from rest_framework.views import exception_handler response = exception_handler(exception, context) From 11aceeef3e968e8c4410dccc914ceaab1e07e77d Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Fri, 7 Aug 2026 15:59:38 +0530 Subject: [PATCH 2/6] feat: include todo count in fixing and affected-by endpoint Signed-off-by: Keshav Priyadarshi --- vulnerabilities/api_v3.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index 36a841cce..4a3a5029b 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -383,6 +383,7 @@ class Meta: "risk_score", "related_ssvc_trees", "fixed_by_packages", + "todo_count", ] @@ -469,7 +470,16 @@ def get_queryset(self): if not purl: return AdvisoryV2.objects.none() - return AdvisoryV2.objects.filter(**{self.relation: purl}).latest_per_avid() + return ( + AdvisoryV2.objects.filter(**{self.relation: purl}) + .latest_per_avid() + .annotate( + todo_count=Count( + "advisory_todos", + filter=Q(advisory_todos__is_todo_stale=False), + ) + ) + ) class FixingAdvisoriesViewSet(PackageAdvisoriesViewSet): @@ -637,7 +647,7 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit max_exploitability=Max( "members__advisory__exploitability", ), - todo_count=Count( + primary_adv_todo_count=Count( "primary_advisory__advisory_todos", filter=Q(primary_advisory__advisory_todos__is_todo_stale=False), ), @@ -786,7 +796,7 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit ), "ssvc_trees": adv.ssvc_trees, "resource_url": resource_url, - "todo_count": adv.todo_count, + "todo_count": adv.primary_adv_todo_count, } ) From 3c10f6223bcfe8996c87f856de5c462d5818fcc5 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Fri, 7 Aug 2026 16:02:07 +0530 Subject: [PATCH 3/6] test: add coverage for advisory todo count in api response Signed-off-by: Keshav Priyadarshi --- vulnerabilities/tests/test_api_v3.py | 137 ++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/vulnerabilities/tests/test_api_v3.py b/vulnerabilities/tests/test_api_v3.py index f12e2fc0f..63a8315a0 100644 --- a/vulnerabilities/tests/test_api_v3.py +++ b/vulnerabilities/tests/test_api_v3.py @@ -23,9 +23,13 @@ from vulnerabilities.importer import PackageCommitPatchData from vulnerabilities.models import AdvisorySet from vulnerabilities.models import AdvisorySetMember +from vulnerabilities.models import AdvisoryToDoV2 from vulnerabilities.models import ImpactedPackage from vulnerabilities.models import ImpactedPackageAffecting from vulnerabilities.models import PackageV2 +from vulnerabilities.pipelines.v2_improvers.group_advisories_for_packages import ( + GroupAdvisoriesForPackages, +) from vulnerabilities.pipes.advisory import insert_advisory_v2 from vulnerabilities.tests.pipelines import TestLogger @@ -241,7 +245,7 @@ def test_advisories_post(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data["results"]), 100) advisory = response.data["results"][0] - self.assertEqual(advisory["advisory_id"], "GHSA-12341") + self.assertContains(response, "GHSA-12341") class APIV3TestCaseOneAdvisoryMultiplePackages(APITestCase): @@ -606,3 +610,134 @@ def test_returns_distinct_types_and_caches_response(self): assert response.json() == ["npm", "pypi"] assert cache.get("package_types") == ["npm", "pypi"] + + +class APIV3TestCaseAdvisoryTODO(APITestCase): + def setUp(self): + logger = TestLogger() + from vulnerabilities.importer import AdvisoryDataV2 + from vulnerabilities.importer import AffectedPackageV2 + + affected_packages1 = [ + ( + AffectedPackageV2( + package=PackageURL(type="pypi", name=f"sample"), + fixed_version_range=PypiVersionRange.from_string("vers:pypi/=1.0.0"), + ) + ) + ] + + advisory1 = AdvisoryDataV2( + advisory_id="GHSA-4321", + aliases=["CVE-2021-4321"], + summary="Sample advisory", + affected_packages=affected_packages1, + url="https://example.com/advisory", + original_advisory_text="Sample advisory text", + ) + advisory_obj1 = insert_advisory_v2(advisory1, "ghsa_importer", logger.write, "ghsa", 100) + cur = timezone.now() + advisory_obj1._all_impacts_unfurled_at = cur + advisory_obj1.save() + + affected_packages = [ + ( + AffectedPackageV2( + package=PackageURL(type="pypi", name=f"sample"), + affected_version_range=PypiVersionRange.from_string("vers:pypi/=1.0.0"), + ) + ) + ] + + advisory = AdvisoryDataV2( + advisory_id="GHSA-1234", + aliases=["CVE-2021-1234"], + summary="Sample advisory", + affected_packages=affected_packages, + url="https://example.com/advisory", + original_advisory_text="Sample advisory text", + ) + + advisory_obj = insert_advisory_v2(advisory, "ghsa_importer", logger.write, "ghsa", 100) + cur = timezone.now() + advisory_obj._all_impacts_unfurled_at = cur + advisory_obj.save() + + todo = AdvisoryToDoV2.objects.create( + related_advisories_id="abrakadabra", + alias="GHSA-1234", + advisories_count=1, + issue_type="CONFLICTING_AFFECTED_PACKAGES", + ) + todo.advisories.add(advisory_obj) + + GroupAdvisoriesForPackages().execute() + + self.client = APIClient(enforce_csrf_checks=True) + + self.allow_request_patcher = patch( + "vulnerabilities.throttling.PermissionBasedUserRateThrottle.allow_request", + return_value=True, + ) + self.allow_request_patcher.start() + self.addCleanup(self.allow_request_patcher.stop) + + self.anon_patcher = patch( + "rest_framework.throttling.AnonRateThrottle.allow_request", + return_value=True, + ) + self.anon_patcher.start() + self.addCleanup(self.anon_patcher.stop) + + def test_get_todo_count_in_package_endpoint(self): + url = reverse("package-v3-list") + + with self.assertNumQueries(13): + response = self.client.post( + url, + data={ + "purls": ["pkg:pypi/sample@1.0.0"], + "details": True, + }, + format="json", + HTTP_USER_AGENT="VCIO_API_AGENT", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + results = response.data["results"] + self.assertEqual(results[0]["affected_by_vulnerabilities"][0]["todo_count"], 1) + + def test_get_todo_count_in_affected_by_advisory_endpoint(self): + url = reverse("affected-by-advisories-list") + with self.assertNumQueries(11): + response = self.client.get( + url, + data={ + "purl": "pkg:pypi/sample@1.0.0", + }, + HTTP_USER_AGENT="VCIO_API_AGENT", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + results = response.data["results"] + self.assertEqual(results[0]["todo_count"], 1) + + def test_get_todo_count_in_advisory_endpoint(self): + url = reverse("fixing-advisories-list") + + with self.assertNumQueries(10): + response = self.client.get( + url, + data={ + "purl": "pkg:pypi/sample@1.0.0", + }, + HTTP_USER_AGENT="VCIO_API_AGENT", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + results = response.data["results"] + print(results) + self.assertEqual(results[0]["todo_count"], 0) From cc79e5efcf4b9613f34c353b9a486001c4cf797c Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Fri, 7 Aug 2026 23:38:23 +0530 Subject: [PATCH 4/6] feat: add fields to track todos associated with curation advisory Signed-off-by: Keshav Priyadarshi --- ...2_is_curation_advisoryv2_resolves_todos.py | 31 +++++++++++++++++++ vulnerabilities/models.py | 14 +++++++++ 2 files changed, 45 insertions(+) create mode 100644 vulnerabilities/migrations/0142_advisoryv2_is_curation_advisoryv2_resolves_todos.py diff --git a/vulnerabilities/migrations/0142_advisoryv2_is_curation_advisoryv2_resolves_todos.py b/vulnerabilities/migrations/0142_advisoryv2_is_curation_advisoryv2_resolves_todos.py new file mode 100644 index 000000000..f0c9bca1e --- /dev/null +++ b/vulnerabilities/migrations/0142_advisoryv2_is_curation_advisoryv2_resolves_todos.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.11 on 2026-08-06 17:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0141_advisorymitigations"), + ] + + operations = [ + migrations.AddField( + model_name="advisoryv2", + name="is_curation", + field=models.BooleanField( + db_index=True, + default=False, + help_text="Indicates whether this is a curation advisory.", + ), + ), + migrations.AddField( + model_name="advisoryv2", + name="resolves_todos", + field=models.ManyToManyField( + help_text="A list of Advisory ToDos resolved by this advisory.", + related_name="resolved_in_advisories", + to="vulnerabilities.advisorytodov2", + ), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 826d35a54..5f0cfa4ea 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -3213,6 +3213,20 @@ class AdvisoryV2(models.Model): help_text="A list of patches associated with this advisory.", ) + resolves_todos = models.ManyToManyField( + AdvisoryToDoV2, + related_name="resolved_in_advisories", + help_text="A list of Advisory ToDos resolved by this advisory.", + ) + + is_curation = models.BooleanField( + default=False, + blank=False, + null=False, + db_index=True, + help_text="Indicates whether this is a curation advisory.", + ) + date_published = models.DateTimeField( blank=True, null=True, From 0494f3383a89a96c79e21d66f53b0df93b3f2bd8 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Fri, 7 Aug 2026 23:35:01 +0530 Subject: [PATCH 5/6] feat: add list of curating advisory in api response Signed-off-by: Keshav Priyadarshi --- vulnerabilities/api_v3.py | 72 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index 4a3a5029b..71d41f5f9 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -35,6 +35,7 @@ from vulnerabilities.models import AdvisorySet from vulnerabilities.models import AdvisorySetMember from vulnerabilities.models import AdvisorySeverity +from vulnerabilities.models import AdvisoryToDoV2 from vulnerabilities.models import AdvisoryV2 from vulnerabilities.models import AdvisoryWeakness from vulnerabilities.models import ImpactedPackageAffecting @@ -128,6 +129,7 @@ class AdvisoryV3Serializer(serializers.ModelSerializer): advisory_uid = serializers.CharField(source="avid", read_only=True) related_ssvc_trees = serializers.SerializerMethodField() todo_count = serializers.IntegerField(read_only=True) + curating_advisories = serializers.SerializerMethodField() def get_related_ssvc_trees(self, obj): seen = set() @@ -152,6 +154,18 @@ def get_related_ssvc_trees(self, obj): return result + def get_curating_advisories(self, obj): + request = self.context.get("request") + return [ + reverse( + "advisory_details", + kwargs={"avid": related_advisory.avid}, + request=request, + ) + for todo in obj.resolves_todos.all() + for related_advisory in todo.advisories.all() + ] + class Meta: model = AdvisoryV2 fields = [ @@ -168,6 +182,8 @@ class Meta: "risk_score", "related_ssvc_trees", "todo_count", + "is_curation", + "curating_advisories", ] @@ -384,6 +400,8 @@ class Meta: "related_ssvc_trees", "fixed_by_packages", "todo_count", + "is_curation", + "curating_advisories", ] @@ -451,6 +469,15 @@ def create(self, request, *args, **kwargs): "source_advisory__url", ), ), + Prefetch( + "resolves_todos", + queryset=AdvisoryToDoV2.objects.prefetch_related( + Prefetch( + "advisories", + queryset=AdvisoryV2.objects.only("avid"), + ) + ), + ), ) ) @@ -479,6 +506,17 @@ def get_queryset(self): filter=Q(advisory_todos__is_todo_stale=False), ) ) + .prefetch_related( + Prefetch( + "resolves_todos", + queryset=AdvisoryToDoV2.objects.prefetch_related( + Prefetch( + "advisories", + queryset=AdvisoryV2.objects.only("avid"), + ) + ), + ), + ) ) @@ -639,6 +677,15 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit "aliases", queryset=AdvisoryAlias.objects.only("alias"), ), + Prefetch( + "primary_advisory__resolves_todos", + queryset=AdvisoryToDoV2.objects.prefetch_related( + Prefetch( + "advisories", + queryset=AdvisoryV2.objects.only("avid"), + ) + ), + ), ) .annotate( max_severity=Max( @@ -658,6 +705,7 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit "primary_advisory__avid", "primary_advisory__summary", "primary_advisory__advisory_id", + "primary_advisory__is_curation", ) ) @@ -770,6 +818,12 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit aliases = [a for a in adv._aliases_cache if a != identifier] + curating_advisories = [ + f"{base_url}{related_advisory.get_absolute_url()}" + for todo in primary.resolves_todos.all() + for related_advisory in todo.advisories.all() + ] + resource_url = None advisory_url = primary.get_absolute_url() @@ -797,6 +851,8 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit "ssvc_trees": adv.ssvc_trees, "resource_url": resource_url, "todo_count": adv.primary_adv_todo_count, + "is_curation": primary.is_curation, + "curating_advisories": curating_advisories, } ) @@ -861,6 +917,15 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit ), to_attr="prefetched_ssvc_trees", ), + Prefetch( + "resolves_todos", + queryset=AdvisoryToDoV2.objects.prefetch_related( + Prefetch( + "advisories", + queryset=AdvisoryV2.objects.only("avid"), + ) + ), + ), ) ) @@ -885,6 +950,11 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit identifier = advisory.advisory_id.split("/")[-1] aliases = [alias.alias for alias in advisory.aliases.all() if alias.alias != identifier] + curating_advisories = [ + f"{base_url}{related_advisory.get_absolute_url()}" + for todo in advisory.resolves_todos.all() + for related_advisory in todo.advisories.all() + ] resource_url = None advisory_url = advisory.get_absolute_url() @@ -915,6 +985,8 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit ], "resource_url": resource_url, "todo_count": advisory.todo_count, + "is_curation": advisory.is_curation, + "curating_advisories": curating_advisories, } ) From 5c64c01906982e1bb08a8dee141e808ca755e3b9 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Fri, 7 Aug 2026 23:35:59 +0530 Subject: [PATCH 6/6] test: add coverage for curating advisory Signed-off-by: Keshav Priyadarshi --- vulnerabilities/tests/test_api_v3.py | 167 ++++++++++++++++-- vulnerabilities/tests/test_data_migrations.py | 6 +- 2 files changed, 160 insertions(+), 13 deletions(-) diff --git a/vulnerabilities/tests/test_api_v3.py b/vulnerabilities/tests/test_api_v3.py index 63a8315a0..09579a8a3 100644 --- a/vulnerabilities/tests/test_api_v3.py +++ b/vulnerabilities/tests/test_api_v3.py @@ -116,7 +116,7 @@ def test_packages_post_with_details(self): def test_advisories_post(self): url = reverse("advisory-v3-list") - with self.assertNumQueries(10): + with self.assertNumQueries(11): response = self.client.post( url, data={"purls": ["pkg:pypi/sample@1.0.0"]}, @@ -133,7 +133,7 @@ def test_advisories_post(self): def test_affected_by_advisories_list(self): url = reverse("affected-by-advisories-list") - with self.assertNumQueries(11): + with self.assertNumQueries(12): response = self.client.get( url, {"purl": "pkg:pypi/sample@1.0.0"}, HTTP_USER_AGENT="VCIO_API_AGENT" ) @@ -234,7 +234,7 @@ def setUp(self): def test_advisories_post(self): url = reverse("advisory-v3-list") - with self.assertNumQueries(10): + with self.assertNumQueries(11): response = self.client.post( url, data={"purls": ["pkg:pypi/sample@1.0.0"]}, @@ -664,7 +664,7 @@ def setUp(self): advisory_obj.save() todo = AdvisoryToDoV2.objects.create( - related_advisories_id="abrakadabra", + related_advisories_id="d1a9088a718d7f6f63676ccead5c", alias="GHSA-1234", advisories_count=1, issue_type="CONFLICTING_AFFECTED_PACKAGES", @@ -692,7 +692,7 @@ def setUp(self): def test_get_todo_count_in_package_endpoint(self): url = reverse("package-v3-list") - with self.assertNumQueries(13): + with self.assertNumQueries(14): response = self.client.post( url, data={ @@ -708,9 +708,10 @@ def test_get_todo_count_in_package_endpoint(self): results = response.data["results"] self.assertEqual(results[0]["affected_by_vulnerabilities"][0]["todo_count"], 1) - def test_get_todo_count_in_affected_by_advisory_endpoint(self): + def test_get_todo_count_in_affected_by_advisories_endpoint(self): url = reverse("affected-by-advisories-list") - with self.assertNumQueries(11): + + with self.assertNumQueries(12): response = self.client.get( url, data={ @@ -724,10 +725,10 @@ def test_get_todo_count_in_affected_by_advisory_endpoint(self): results = response.data["results"] self.assertEqual(results[0]["todo_count"], 1) - def test_get_todo_count_in_advisory_endpoint(self): + def test_get_todo_count_in_fixing_advisories_endpoint(self): url = reverse("fixing-advisories-list") - with self.assertNumQueries(10): + with self.assertNumQueries(11): response = self.client.get( url, data={ @@ -739,5 +740,151 @@ def test_get_todo_count_in_advisory_endpoint(self): self.assertEqual(response.status_code, status.HTTP_200_OK) results = response.data["results"] - print(results) self.assertEqual(results[0]["todo_count"], 0) + + +class APIV3TestCaseCuratedAdvisory(APITestCase): + def setUp(self): + logger = TestLogger() + from vulnerabilities.importer import AdvisoryDataV2 + from vulnerabilities.importer import AffectedPackageV2 + + affected_packages1 = [ + ( + AffectedPackageV2( + package=PackageURL(type="pypi", name=f"sample"), + fixed_version_range=PypiVersionRange.from_string("vers:pypi/=1.0.0"), + ) + ) + ] + + advisory1 = AdvisoryDataV2( + advisory_id="GHSA-4321", + aliases=["CVE-2021-4321"], + summary="Sample advisory", + affected_packages=affected_packages1, + url="https://example.com/advisory", + original_advisory_text="Sample advisory text", + ) + advisory_obj1 = insert_advisory_v2(advisory1, "ghsa_importer", logger.write, "ghsa", 100) + cur = timezone.now() + advisory_obj1._all_impacts_unfurled_at = cur + advisory_obj1.save() + + todo = AdvisoryToDoV2.objects.create( + related_advisories_id="d1a9088a718d7f6f63676ccead5c", + alias="GHSA-4321", + advisories_count=1, + issue_type="CONFLICTING_AFFECTED_PACKAGES", + ) + todo.advisories.add(advisory_obj1) + + affected_packages = [ + ( + AffectedPackageV2( + package=PackageURL(type="pypi", name=f"sample"), + affected_version_range=PypiVersionRange.from_string("vers:pypi/=1.0.0"), + ) + ) + ] + + advisory = AdvisoryDataV2( + advisory_id="curation/GHSA-1234", + aliases=["CVE-2021-1234"], + summary="Sample advisory", + affected_packages=affected_packages, + url="https://example.com/advisory", + original_advisory_text="Sample advisory text", + ) + + advisory_obj = insert_advisory_v2( + advisory, "curation_importer", logger.write, "curation", 100 + ) + cur = timezone.now() + advisory_obj._all_impacts_unfurled_at = cur + advisory_obj.is_curation = True + advisory_obj.save() + advisory_obj.resolves_todos.add(todo) + + GroupAdvisoriesForPackages().execute() + + self.client = APIClient(enforce_csrf_checks=True) + + self.allow_request_patcher = patch( + "vulnerabilities.throttling.PermissionBasedUserRateThrottle.allow_request", + return_value=True, + ) + self.allow_request_patcher.start() + self.addCleanup(self.allow_request_patcher.stop) + + self.anon_patcher = patch( + "rest_framework.throttling.AnonRateThrottle.allow_request", + return_value=True, + ) + self.anon_patcher.start() + self.addCleanup(self.anon_patcher.stop) + + def test_get_curating_advisories_in_package_endpoint(self): + url = reverse("package-v3-list") + + with self.assertNumQueries(15): + response = self.client.post( + url, + data={ + "purls": ["pkg:pypi/sample@1.0.0"], + "details": True, + }, + format="json", + HTTP_USER_AGENT="VCIO_API_AGENT", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + results = response.data["results"] + affected_by_vulnerabilities = results[0]["affected_by_vulnerabilities"][0] + + self.assertEqual(affected_by_vulnerabilities["is_curation"], True) + self.assertEqual( + affected_by_vulnerabilities["curating_advisories"], + ["http://testserver/advisories/ghsa/GHSA-4321"], + ) + + def test_get_curating_advisories_in_affected_by_advisory_endpoint(self): + url = reverse("affected-by-advisories-list") + + with self.assertNumQueries(13): + response = self.client.get( + url, + data={ + "purl": "pkg:pypi/sample@1.0.0", + }, + HTTP_USER_AGENT="VCIO_API_AGENT", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + results = response.data["results"][0] + + self.assertEqual(results["is_curation"], True) + self.assertEqual( + results["curating_advisories"], ["http://testserver/advisories/ghsa/GHSA-4321"] + ) + + def test_get_curating_advisories_in_advisory_endpoint(self): + url = reverse("fixing-advisories-list") + + with self.assertNumQueries(11): + response = self.client.get( + url, + data={ + "purl": "pkg:pypi/sample@1.0.0", + }, + HTTP_USER_AGENT="VCIO_API_AGENT", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + results = response.data["results"][0] + + self.assertEqual(results["is_curation"], False) + self.assertEqual(results["curating_advisories"], []) diff --git a/vulnerabilities/tests/test_data_migrations.py b/vulnerabilities/tests/test_data_migrations.py index 6d22fdfbc..ceedde20f 100644 --- a/vulnerabilities/tests/test_data_migrations.py +++ b/vulnerabilities/tests/test_data_migrations.py @@ -1390,9 +1390,9 @@ class TestCleanAdvisorySeverityMigration(TestMigrations): migrate_to = "0139_cleanup_none_string_in_severity" def setUpBeforeMigration(self, apps): - # AdvisoryV2 = apps.get_model("vulnerabilities", "AdvisoryV2") - # ImpactedPackage = apps.get_model("vulnerabilities", "ImpactedPackage") - # AdvisorySeverity = apps.get_model("vulnerabilities", "AdvisorySeverity") + AdvisoryV2 = apps.get_model("vulnerabilities", "AdvisoryV2") + ImpactedPackage = apps.get_model("vulnerabilities", "ImpactedPackage") + AdvisorySeverity = apps.get_model("vulnerabilities", "AdvisorySeverity") self.advisory1 = AdvisoryV2.objects.create( unique_content_id="b001d1a8952bc056d0161f1dd45dd8f90b25f62c56a887ea21d09fafd78a0f61",