Skip to content

Commit 5aca627

Browse files
authored
Add advisory codefix V2 URL (#1926)
* Add advisory codefix V2 URL Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * Add num query tests for codefix viewset Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * Add num query tests for codefix viewset Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> * Modify CHANGELOG Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com> --------- Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 3993290 commit 5aca627

File tree

7 files changed

+246
-13
lines changed

7 files changed

+246
-13
lines changed

CHANGELOG.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
Release notes
22
=============
33

4+
Version v37.0.0
5+
---------------------
6+
7+
- This is a major version, this version introduces Advisory level details
8+
https://github.com/aboutcode-org/vulnerablecode/issues/1796
9+
https://github.com/aboutcode-org/vulnerablecode/issues/1393
10+
https://github.com/aboutcode-org/vulnerablecode/issues/1883
11+
https://github.com/aboutcode-org/vulnerablecode/issues/1882
12+
https://github.com/aboutcode-org/vulnerablecode/pull/1866
13+
- We have added new models AdvisoryV2, AdvisoryAlias, AdvisoryReference, AdvisorySeverity, AdvisoryWeakness, PackageV2 and CodeFixV2.
14+
- We are using ``avid`` as an internal advisory ID for uniquely identifying advisories.
15+
- We have a new route ``/v2`` which only support package search which has information on packages that are reported to be affected or fixing by advisories.
16+
- This version introduces ``/api/v2/advisories-packages`` which has information on packages that are reported to be affected or fixing by advisories.
17+
- Pipeline Dashboard improvements #1920.
18+
- Throttle API requests based on user permissions #1909.
19+
- Add pipeline to compute Advisory ToDos #1764
20+
421
Version v36.1.3
522
---------------------
623

vulnerabilities/api_v2.py

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ def get_affected_by_vulnerabilities(self, obj):
334334
# Get code fixed for a vulnerability
335335
code_fixes = CodeFixV2.objects.filter(advisory=adv).distinct()
336336
code_fix_urls = [
337-
reverse("codefix-detail", args=[code_fix.id], request=request)
337+
reverse("advisory-codefix-detail", args=[code_fix.id], request=request)
338338
for code_fix in code_fixes
339339
]
340340

@@ -718,6 +718,58 @@ class Meta:
718718
read_only_fields = ["created_at", "updated_at"]
719719

720720

721+
class CodeFixV2Serializer(serializers.ModelSerializer):
722+
"""
723+
Serializer for the CodeFix model.
724+
Provides detailed information about a code fix.
725+
"""
726+
727+
affected_advisory_id = serializers.CharField(
728+
source="advisory.avid",
729+
read_only=True,
730+
help_text="ID of the advisory affecting the package.",
731+
)
732+
affected_package_purl = serializers.CharField(
733+
source="affected_package.package_url",
734+
read_only=True,
735+
help_text="PURL of the affected package.",
736+
)
737+
fixed_package_purl = serializers.CharField(
738+
source="fixed_package.package_url",
739+
read_only=True,
740+
help_text="PURL of the fixing package (if available).",
741+
)
742+
created_at = serializers.DateTimeField(
743+
format="%Y-%m-%dT%H:%M:%SZ",
744+
read_only=True,
745+
help_text="Timestamp when the code fix was created.",
746+
)
747+
updated_at = serializers.DateTimeField(
748+
format="%Y-%m-%dT%H:%M:%SZ",
749+
read_only=True,
750+
help_text="Timestamp when the code fix was last updated.",
751+
)
752+
753+
class Meta:
754+
model = CodeFixV2
755+
fields = [
756+
"id",
757+
"commits",
758+
"pulls",
759+
"downloads",
760+
"patch",
761+
"affected_advisory_id",
762+
"affected_package_purl",
763+
"fixed_package_purl",
764+
"notes",
765+
"references",
766+
"is_reviewed",
767+
"created_at",
768+
"updated_at",
769+
]
770+
read_only_fields = ["created_at", "updated_at"]
771+
772+
721773
class CodeFixViewSet(viewsets.ReadOnlyModelViewSet):
722774
"""
723775
API endpoint that allows viewing CodeFix entries.
@@ -740,6 +792,25 @@ def get_queryset(self):
740792
return queryset
741793

742794

795+
class CodeFixV2ViewSet(viewsets.ReadOnlyModelViewSet):
796+
"""
797+
API endpoint that allows viewing CodeFix entries.
798+
"""
799+
800+
queryset = CodeFixV2.objects.all()
801+
serializer_class = CodeFixV2Serializer
802+
803+
def get_queryset(self):
804+
"""
805+
Optionally filter by vulnerability ID.
806+
"""
807+
queryset = super().get_queryset()
808+
advisory_id = self.request.query_params.get("advisory_id")
809+
if advisory_id:
810+
queryset = queryset.filter(advisory__avid=advisory_id)
811+
return queryset
812+
813+
743814
class CreateListRetrieveUpdateViewSet(
744815
mixins.CreateModelMixin,
745816
mixins.ListModelMixin,
@@ -1061,10 +1132,10 @@ def bulk_search(self, request):
10611132
# Collect vulnerabilities associated with these packages
10621133
advisories = set()
10631134
for package in packages:
1064-
advisories.update(package.affected_by_vulnerabilities.all())
1065-
advisories.update(package.fixing_vulnerabilities.all())
1135+
advisories.update(package.affected_by_advisories.all())
1136+
advisories.update(package.fixing_advisories.all())
10661137

1067-
advisory_data = {adv.avid: VulnerabilityV2Serializer(adv).data for adv in advisories}
1138+
advisory_data = {adv.avid: AdvisoryV2Serializer(adv).data for adv in advisories}
10681139

10691140
if not purl_only:
10701141
package_data = AdvisoryPackageV2Serializer(
@@ -1089,8 +1160,8 @@ def bulk_search(self, request):
10891160
# Collect vulnerabilities associated with these packages
10901161
advisories = set()
10911162
for package in packages:
1092-
advisories.update(package.affected_by_vulnerabilities.all())
1093-
advisories.update(package.fixing_vulnerabilities.all())
1163+
advisories.update(package.affected_by_advisories.all())
1164+
advisories.update(package.fixing_advisories.all())
10941165

10951166
advisory_data = {adv.advisory_id: AdvisoryV2Serializer(adv).data for adv in advisories}
10961167

vulnerabilities/models.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,14 @@ class VulnerabilityStatusType(models.IntegerChoices):
222222
INVALID = 3, "Invalid"
223223

224224

225+
class AdvisoryStatusType(models.IntegerChoices):
226+
"""List of vulnerability statuses."""
227+
228+
PUBLISHED = 1, "Published"
229+
DISPUTED = 2, "Disputed"
230+
INVALID = 3, "Invalid"
231+
232+
225233
# FIXME: Remove when migration from Vulnerability to Advisory is completed
226234
class Vulnerability(models.Model):
227235
"""
@@ -2738,7 +2746,7 @@ class AdvisoryV2(models.Model):
27382746
)
27392747

27402748
status = models.IntegerField(
2741-
choices=VulnerabilityStatusType.choices, default=VulnerabilityStatusType.PUBLISHED
2749+
choices=AdvisoryStatusType.choices, default=AdvisoryStatusType.PUBLISHED
27422750
)
27432751

27442752
exploitability = models.DecimalField(
@@ -2789,7 +2797,7 @@ def get_absolute_url(self):
27892797
"""
27902798
Return this Vulnerability details absolute URL.
27912799
"""
2792-
return reverse("advisory_details", args=[self.id])
2800+
return reverse("advisory_details", args=[self.avid])
27932801

27942802
def to_advisory_data(self) -> "AdvisoryDataV2":
27952803
from vulnerabilities.importer import AdvisoryDataV2
@@ -2975,6 +2983,12 @@ def _vulnerable(self, vulnerable=True):
29752983
"""
29762984
return self.with_is_vulnerable().filter(is_vulnerable=vulnerable)
29772985

2986+
def vulnerable(self):
2987+
"""
2988+
Return only packages that are vulnerable.
2989+
"""
2990+
return self.filter(affected_by_advisories__isnull=False)
2991+
29782992
def with_is_vulnerable(self):
29792993
"""
29802994
Annotate Package with ``is_vulnerable`` boolean attribute.
@@ -2983,6 +2997,12 @@ def with_is_vulnerable(self):
29832997
is_vulnerable=Exists(AdvisoryV2.objects.filter(affecting_packages__pk=OuterRef("pk")))
29842998
)
29852999

3000+
def from_purl(self, purl: Union[PackageURL, str]):
3001+
"""
3002+
Return a new Package given a ``purl`` PackageURL object or PURL string.
3003+
"""
3004+
return PackageV2.objects.create(**purl_to_dict(purl=purl))
3005+
29863006

29873007
class PackageV2(PackageURLMixin):
29883008
"""

vulnerabilities/templates/package_details_v2.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@
146146
{% for advisory in affected_by_advisories %}
147147
<tr>
148148
<td>
149-
<a href="/advisories/{{advisory.id}}">
149+
<a href="{{advisory.get_absolute_url}}">
150150
{{advisory.avid }}
151151
</a>
152152
<br />
@@ -267,7 +267,7 @@
267267
{% for advisory in fixing_advisories %}
268268
<tr>
269269
<td>
270-
<a href="/advisories/{{advisory.id}}">
270+
<a href="{{advisory.get_absolute_url}}">
271271
{{advisory.avid }}
272272
</a>
273273
</td>

vulnerabilities/tests/test_api_v2.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919

2020
from vulnerabilities.api_v2 import PackageV2Serializer
2121
from vulnerabilities.api_v2 import VulnerabilityListSerializer
22+
from vulnerabilities.models import AdvisoryV2
2223
from vulnerabilities.models import Alias
2324
from vulnerabilities.models import ApiUser
25+
from vulnerabilities.models import CodeFixV2
2426
from vulnerabilities.models import Package
27+
from vulnerabilities.models import PackageV2
2528
from vulnerabilities.models import PipelineRun
2629
from vulnerabilities.models import PipelineSchedule
2730
from vulnerabilities.models import Vulnerability
@@ -782,3 +785,123 @@ def test_schedule_update_with_staff_session_permitted(self, mock_create_new_job)
782785
self.assertEqual(response.status_code, status.HTTP_200_OK)
783786
self.assertNotEqual(response.status_code, status.HTTP_403_FORBIDDEN)
784787
self.assertEqual(self.schedule1.run_interval, 2)
788+
789+
790+
class CodeFixV2APITest(APITestCase):
791+
def setUp(self):
792+
self.advisory = AdvisoryV2.objects.create(
793+
datasource_id="test_source",
794+
advisory_id="TEST-2025-001",
795+
avid="test_source/TEST-2025-001",
796+
unique_content_id="a" * 64,
797+
url="https://example.com/advisory",
798+
date_collected="2025-07-01T00:00:00Z",
799+
)
800+
801+
self.affected_package = PackageV2.objects.from_purl(purl="pkg:pypi/affected_package@1.0.0")
802+
self.fixed_package = PackageV2.objects.from_purl(purl="pkg:pypi/fixed_package@1.0.1")
803+
804+
self.codefix = CodeFixV2.objects.create(
805+
advisory=self.advisory,
806+
affected_package=self.affected_package,
807+
fixed_package=self.fixed_package,
808+
notes="Security patch",
809+
is_reviewed=True,
810+
)
811+
self.user = ApiUser.objects.create_api_user(username="e@mail.com")
812+
self.auth = f"Token {self.user.auth_token.key}"
813+
self.client = APIClient(enforce_csrf_checks=True)
814+
self.client.credentials(HTTP_AUTHORIZATION=self.auth)
815+
816+
self.url = reverse("advisory-codefix-list")
817+
818+
def test_list_all_codefixes(self):
819+
with self.assertNumQueries(10):
820+
response = self.client.get(self.url)
821+
assert response.status_code == status.HTTP_200_OK
822+
assert response.data["count"] == 1
823+
assert response.data["results"][0]["affected_advisory_id"] == self.advisory.avid
824+
825+
def test_filter_codefix_by_advisory_id_success(self):
826+
with self.assertNumQueries(10):
827+
response = self.client.get(self.url, {"advisory_id": self.advisory.avid})
828+
assert response.status_code == status.HTTP_200_OK
829+
assert response.data["count"] == 1
830+
assert response.data["results"][0]["affected_advisory_id"] == self.advisory.avid
831+
832+
def test_filter_codefix_by_advisory_id_not_found(self):
833+
with self.assertNumQueries(6):
834+
response = self.client.get(self.url, {"advisory_id": "nonexistent/ADVISORY-ID"})
835+
assert response.status_code == status.HTTP_200_OK
836+
assert response.data["count"] == 0
837+
838+
839+
class AdvisoriesPackageV2Tests(APITestCase):
840+
def setUp(self):
841+
self.advisory = AdvisoryV2.objects.create(
842+
datasource_id="ghsa",
843+
advisory_id="GHSA-1234",
844+
avid="ghsa/GHSA-1234",
845+
unique_content_id="f" * 64,
846+
url="https://example.com/advisory",
847+
date_collected="2025-07-01T00:00:00Z",
848+
)
849+
850+
self.package = PackageV2.objects.from_purl(purl="pkg:pypi/sample@1.0.0")
851+
852+
self.user = ApiUser.objects.create_api_user(username="e@mail.com")
853+
self.auth = f"Token {self.user.auth_token.key}"
854+
self.client = APIClient(enforce_csrf_checks=True)
855+
self.client.credentials(HTTP_AUTHORIZATION=self.auth)
856+
857+
self.package.affected_by_advisories.add(self.advisory)
858+
self.package.save()
859+
860+
def test_list_with_purl_filter(self):
861+
url = reverse("advisories-package-v2-list")
862+
with self.assertNumQueries(18):
863+
response = self.client.get(url, {"purl": "pkg:pypi/sample@1.0.0"})
864+
assert response.status_code == 200
865+
assert "packages" in response.data["results"]
866+
assert "advisories" in response.data["results"]
867+
assert self.advisory.avid in response.data["results"]["advisories"]
868+
869+
def test_bulk_lookup(self):
870+
url = reverse("advisories-package-v2-bulk-lookup")
871+
with self.assertNumQueries(13):
872+
response = self.client.post(url, {"purls": ["pkg:pypi/sample@1.0.0"]}, format="json")
873+
assert response.status_code == 200
874+
assert "packages" in response.data
875+
assert "advisories" in response.data
876+
assert self.advisory.avid in response.data["advisories"]
877+
878+
def test_bulk_search_plain(self):
879+
url = reverse("advisories-package-v2-bulk-search")
880+
payload = {"purls": ["pkg:pypi/sample@1.0.0"], "plain_purl": True, "purl_only": False}
881+
with self.assertNumQueries(13):
882+
response = self.client.post(url, payload, format="json")
883+
assert response.status_code == 200
884+
assert "packages" in response.data
885+
assert "advisories" in response.data
886+
887+
def test_bulk_search_purl_only(self):
888+
url = reverse("advisories-package-v2-bulk-search")
889+
payload = {"purls": ["pkg:pypi/sample@1.0.0"], "plain_purl": False, "purl_only": True}
890+
with self.assertNumQueries(13):
891+
response = self.client.post(url, payload, format="json")
892+
assert response.status_code == 200
893+
assert "pkg:pypi/sample@1.0.0" in response.data
894+
895+
def test_lookup_single_package(self):
896+
url = reverse("advisories-package-v2-lookup")
897+
with self.assertNumQueries(11):
898+
response = self.client.post(url, {"purl": "pkg:pypi/sample@1.0.0"}, format="json")
899+
assert response.status_code == 200
900+
assert any(pkg["purl"] == "pkg:pypi/sample@1.0.0" for pkg in response.data)
901+
902+
def test_get_all_vulnerable_purls(self):
903+
url = reverse("advisories-package-v2-all")
904+
with self.assertNumQueries(6):
905+
response = self.client.get(url)
906+
assert response.status_code == 200
907+
assert "pkg:pypi/sample@1.0.0" in response.data

vulnerabilities/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,8 @@ def get_context_data(self, **kwargs):
325325
class AdvisoryDetails(DetailView):
326326
model = models.AdvisoryV2
327327
template_name = "advisory_detail.html"
328-
slug_url_kwarg = "id"
329-
slug_field = "id"
328+
slug_url_kwarg = "avid"
329+
slug_field = "avid"
330330

331331
def get_queryset(self):
332332
return (

vulnerablecode/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from vulnerabilities.api import PackageViewSet
2222
from vulnerabilities.api import VulnerabilityViewSet
2323
from vulnerabilities.api_v2 import AdvisoriesPackageV2ViewSet
24+
from vulnerabilities.api_v2 import CodeFixV2ViewSet
2425
from vulnerabilities.api_v2 import CodeFixViewSet
2526
from vulnerabilities.api_v2 import PackageV2ViewSet
2627
from vulnerabilities.api_v2 import PipelineScheduleV2ViewSet
@@ -67,6 +68,7 @@ def __init__(self, *args, **kwargs):
6768
api_v2_router.register("vulnerabilities", VulnerabilityV2ViewSet, basename="vulnerability-v2")
6869
api_v2_router.register("codefixes", CodeFixViewSet, basename="codefix")
6970
api_v2_router.register("pipelines", PipelineScheduleV2ViewSet, basename="pipelines")
71+
api_v2_router.register("advisory-codefixes", CodeFixV2ViewSet, basename="advisory-codefix")
7072

7173

7274
urlpatterns = [
@@ -102,7 +104,7 @@ def __init__(self, *args, **kwargs):
102104
name="home",
103105
),
104106
path(
105-
"advisories/<int:id>",
107+
"advisories/<path:avid>",
106108
AdvisoryDetails.as_view(),
107109
name="advisory_details",
108110
),

0 commit comments

Comments
 (0)