Skip to content

Commit 9074e2b

Browse files
committed
Add an api test
Use generate_patch_url function from package url Add a filters for api ( package-commit-patches, patches ) Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 438f128 commit 9074e2b

File tree

4 files changed

+219
-59
lines changed

4 files changed

+219
-59
lines changed

vulnerabilities/api_v2.py

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ class Meta:
347347
"id",
348348
"commit_hash",
349349
"vcs_url",
350+
"commit_url",
350351
"patch_url",
351352
"introduced_in_advisories",
352353
"fixed_in_advisories",
@@ -365,7 +366,7 @@ def serialize_impacts(impacts):
365366
unique_pairs = set()
366367
for impact in impacts:
367368
unique_pairs.add((impact.base_purl, impact.advisory.avid))
368-
return [{"package": base_purl, "avid": avid} for base_purl, avid in unique_pairs]
369+
return [{"purl": base_purl, "avid": avid} for base_purl, avid in unique_pairs]
369370

370371

371372
class PatchSerializer(serializers.ModelSerializer):
@@ -376,7 +377,7 @@ class Meta:
376377
fields = ["id", "patch_url", "in_advisories"]
377378

378379
def get_in_advisories(self, obj):
379-
return [{"avid": advisory.avid} for advisory in obj.advisories.all()]
380+
return [advisory.avid for advisory in obj.advisories.all()]
380381

381382

382383
class PackageV3Serializer(serializers.ModelSerializer):
@@ -935,63 +936,65 @@ def get_queryset(self):
935936
return queryset
936937

937938

939+
class PackageCommitPatchFilter(filters.FilterSet):
940+
advisory_avid = filters.CharFilter(method="filter_by_advisory", label="Advisory ID")
941+
purl = filters.CharFilter(method="filter_by_purl", label="Purl")
942+
commit_hash = filters.CharFilter(lookup_expr="exact", label="Commit Hash")
943+
vcs_url = filters.CharFilter(lookup_expr="icontains", label="VCS URL")
944+
945+
class Meta:
946+
model = PackageCommitPatch
947+
fields = ["id", "advisory_avid", "purl", "commit_hash", "vcs_url"]
948+
949+
def filter_by_advisory(self, queryset, name, value):
950+
return queryset.filter(
951+
Q(introduced_in_impacts__advisory__avid=value)
952+
| Q(fixed_in_impacts__advisory__avid=value)
953+
).distinct()
954+
955+
def filter_by_purl(self, queryset, name, value):
956+
return queryset.filter(
957+
Q(introduced_in_impacts__base_purl__icontains=value)
958+
| Q(fixed_in_impacts__base_purl__icontains=value)
959+
).distinct()
960+
961+
938962
class PackageCommitPatchViewSet(viewsets.ReadOnlyModelViewSet):
939963
"""
940-
API endpoint that allows viewing PackageCommitPatch entries.
964+
API endpoint that allows viewing PackageCommitPatch entries
941965
"""
942966

943967
serializer_class = PackageCommitPatchSerializer
944968
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
969+
filter_backends = [filters.DjangoFilterBackend]
970+
filterset_class = PackageCommitPatchFilter
945971

946972
def get_queryset(self):
947-
queryset = PackageCommitPatch.objects.prefetch_related(
973+
return PackageCommitPatch.objects.prefetch_related(
948974
"introduced_in_impacts__advisory", "fixed_in_impacts__advisory"
949-
)
975+
).order_by("id")
950976

951-
pk = self.request.query_params.get("id")
952-
if pk:
953-
queryset = queryset.filter(id=pk)
954977

955-
advisory_id = self.request.query_params.get("advisory_id")
956-
if advisory_id:
957-
queryset = queryset.filter(
958-
Q(introduced_in_impacts__advisory__avid=advisory_id)
959-
| Q(fixed_in_impacts__advisory__avid=advisory_id)
960-
).distinct()
978+
class PatchFilter(filters.FilterSet):
979+
advisory_avid = filters.CharFilter(field_name="advisories__avid", label="Advisory ID")
961980

962-
purl = self.request.query_params.get("purl")
963-
if purl:
964-
queryset = queryset.filter(
965-
Q(introduced_in_impacts__base_purl__icontains=purl)
966-
| Q(fixed_in_impacts__base_purl__icontains=purl)
967-
).distinct()
968-
969-
return queryset.order_by("id")
981+
class Meta:
982+
model = Patch
983+
fields = ["id", "advisory_avid"]
970984

971985

972986
class PatchViewSet(viewsets.ReadOnlyModelViewSet):
973987
"""
974-
API endpoint that allows viewing PackageCommitPatch entries.
988+
API endpoint that allows viewing Patch entries
975989
"""
976990

977991
serializer_class = PatchSerializer
978992
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
993+
filter_backends = [filters.DjangoFilterBackend]
994+
filterset_class = PatchFilter
979995

980996
def get_queryset(self):
981-
queryset = Patch.objects.all()
982-
983-
pk = self.request.query_params.get("id")
984-
if pk:
985-
queryset = queryset.filter(id=pk)
986-
987-
advisory_id = self.request.query_params.get("advisory_id")
988-
if advisory_id:
989-
queryset = queryset.filter(advisory__advisory_id=advisory_id).distinct()
990-
991-
purl = self.request.query_params.get("purl")
992-
if purl:
993-
queryset = queryset.filter(package__package_url__icontains=purl).distinct()
994-
return queryset
997+
return Patch.objects.all()
995998

996999

9971000
class CodeFixV2ViewSet(viewsets.ReadOnlyModelViewSet):

vulnerabilities/tests/test_api_v2.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323
from vulnerabilities.models import Alias
2424
from vulnerabilities.models import ApiUser
2525
from vulnerabilities.models import CodeFixV2
26+
from vulnerabilities.models import ImpactedPackage
2627
from vulnerabilities.models import Package
28+
from vulnerabilities.models import PackageCommitPatch
2729
from vulnerabilities.models import PackageV2
30+
from vulnerabilities.models import Patch
2831
from vulnerabilities.models import PipelineRun
2932
from vulnerabilities.models import PipelineSchedule
3033
from vulnerabilities.models import Vulnerability
@@ -905,3 +908,154 @@ def test_get_all_vulnerable_purls(self):
905908
response = self.client.get(url)
906909
assert response.status_code == 200
907910
assert "pkg:pypi/sample@1.0.0" in response.data
911+
912+
913+
class PackageCommitPatchList(APITestCase):
914+
def setUp(self):
915+
self.advisory = AdvisoryV2.objects.create(
916+
datasource_id="test_source",
917+
advisory_id="TEST-2025-001",
918+
avid="test_source/TEST-2025-001",
919+
unique_content_id="a" * 64,
920+
url="https://example.com/advisory",
921+
date_collected="2025-07-01T00:00:00Z",
922+
)
923+
924+
self.affected_package = PackageV2.objects.from_purl(purl="pkg:github/torvalds/linux@1.0.0")
925+
self.fixed_package = PackageV2.objects.from_purl(purl="pkg:github/torvalds/linux@1.0.1")
926+
927+
self.pkg_commit_patch1 = PackageCommitPatch.objects.create(
928+
commit_hash="2e1c42391ff2556387b3cb6308b24f6f65619feb",
929+
vcs_url="https://github.com/torvalds/linux",
930+
patch_text="From 2e1c42391ff2556387b3cb6308b24f6f65619feb Mon Sep 17 00:00:00 2001...",
931+
)
932+
933+
self.pkg_commit_patch2 = PackageCommitPatch.objects.create(
934+
commit_hash="99253eb750fda6a644d5188fb26c43bad8d5a745",
935+
vcs_url="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git",
936+
patch_text="From 99253eb750fda6a644d5188fb26c43bad8d5a745 Mon Sep 17 00:00:00 2001...",
937+
)
938+
939+
self.pkg_commit_patch3 = PackageCommitPatch.objects.create(
940+
commit_hash="f043bfc98c193c284e2cd768fefabe18ac2fed9b",
941+
vcs_url="https://github.com/torvalds/linux",
942+
patch_text="From f043bfc98c193c284e2cd768fefabe18ac2fed9b Mon Sep 17 00:00:00 2001...",
943+
)
944+
945+
self.impacted_package1 = ImpactedPackage.objects.create(
946+
base_purl="pkg:github/torvalds/linux",
947+
advisory=self.advisory,
948+
)
949+
950+
self.impacted_package2 = ImpactedPackage.objects.create(
951+
base_purl="pkg:generic/git.kernel.org/pub/scm/linux/kernel",
952+
advisory=self.advisory,
953+
)
954+
955+
self.impacted_package1.fixed_by_package_commit_patches.add(self.pkg_commit_patch1)
956+
self.impacted_package1.introduced_by_package_commit_patches.add(self.pkg_commit_patch3)
957+
self.impacted_package2.fixed_by_package_commit_patches.add(self.pkg_commit_patch2)
958+
959+
self.user = ApiUser.objects.create_api_user(username="e@mail.com")
960+
self.auth = f"Token {self.user.auth_token.key}"
961+
self.client = APIClient(enforce_csrf_checks=True)
962+
self.client.credentials(HTTP_AUTHORIZATION=self.auth)
963+
self.url = reverse("package-commit-patch-list")
964+
965+
def test_package_commit_patches_list(self):
966+
response = self.client.get(self.url)
967+
assert response.status_code == 200
968+
results = response.json().get("results", response.json())
969+
assert len(results) == 3
970+
patch_data = results[0]
971+
assert patch_data["vcs_url"] == self.pkg_commit_patch1.vcs_url
972+
assert patch_data["commit_hash"] == self.pkg_commit_patch1.commit_hash
973+
assert patch_data["fixed_in_advisories"] == [
974+
{"avid": self.advisory.avid, "purl": self.impacted_package1.base_purl}
975+
]
976+
assert patch_data["introduced_in_advisories"] == []
977+
978+
def test_filter_by_commit_hash(self):
979+
response = self.client.get(f"{self.url}?commit_hash={self.pkg_commit_patch1.commit_hash}")
980+
results = response.json().get("results", response.json())
981+
assert len(results) == 1
982+
983+
response = self.client.get(f"{self.url}?commit_hash=test")
984+
results = response.json().get("results", response.json())
985+
assert len(results) == 0
986+
987+
def test_filter_by_vcs_url(self):
988+
response = self.client.get(f"{self.url}?vcs_url={self.pkg_commit_patch1.vcs_url}")
989+
results = response.json().get("results", response.json())
990+
assert len(results) == 2
991+
992+
response = self.client.get(f"{self.url}?vcs_url=test")
993+
results = response.json().get("results", response.json())
994+
assert len(results) == 0
995+
996+
def test_filter_by_advisory_avid(self):
997+
response = self.client.get(f"{self.url}?advisory_avid={self.advisory.avid}")
998+
results = response.json().get("results", response.json())
999+
assert len(results) == 3
1000+
1001+
response = self.client.get(f"{self.url}?advisory_avid=test_source/DOES-NOT-EXIST")
1002+
results = response.json().get("results", response.json())
1003+
assert len(results) == 0
1004+
1005+
def test_filter_by_purl(self):
1006+
response = self.client.get(f"{self.url}?purl=pkg:github/torvalds/linux")
1007+
results = response.json().get("results", response.json())
1008+
assert len(results) == 2
1009+
assert any(r["id"] == self.pkg_commit_patch1.id for r in results)
1010+
1011+
response = self.client.get(f"{self.url}?purl=pkg:github/aboutcode-org")
1012+
results = response.json().get("results", response.json())
1013+
assert len(results) == 0
1014+
1015+
def test_filter_by_id(self):
1016+
response = self.client.get(f"{self.url}?id={self.pkg_commit_patch1.id}")
1017+
results = response.json().get("results", response.json())
1018+
assert len(results) == 1
1019+
assert results[0]["id"] == self.pkg_commit_patch1.id
1020+
1021+
response = self.client.get(f"{self.url}?id=51646849")
1022+
results = response.json().get("results", response.json())
1023+
assert len(results) == 0
1024+
1025+
1026+
class PatchList(APITestCase):
1027+
def setUp(self):
1028+
self.advisory = AdvisoryV2.objects.create(
1029+
datasource_id="test_source",
1030+
advisory_id="TEST-2025-001",
1031+
avid="test_source/TEST-2025-001",
1032+
unique_content_id="a" * 64,
1033+
url="https://example.com/advisory",
1034+
date_collected="2025-07-01T00:00:00Z",
1035+
)
1036+
1037+
self.patch = Patch.objects.create(
1038+
patch_url="https://lore.kernel.org/patchwork/patch/1086060/", patch_text="some text"
1039+
)
1040+
1041+
self.advisory.patches.add(self.patch)
1042+
1043+
self.user = ApiUser.objects.create_api_user(username="e@mail.com")
1044+
self.auth = f"Token {self.user.auth_token.key}"
1045+
self.client = APIClient(enforce_csrf_checks=True)
1046+
self.client.credentials(HTTP_AUTHORIZATION=self.auth)
1047+
self.url = reverse("patches-list")
1048+
1049+
def test_patch_list(self):
1050+
response = self.client.get(self.url)
1051+
assert response.status_code == 200
1052+
results = response.json().get("results", response.json())
1053+
assert len(results) == 1
1054+
assert results[0]["patch_url"] == self.patch.patch_url
1055+
assert results == [
1056+
{
1057+
"id": 1,
1058+
"patch_url": "https://lore.kernel.org/patchwork/patch/1086060/",
1059+
"in_advisories": ["test_source/TEST-2025-001"],
1060+
}
1061+
]

vulnerabilities/utils.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from cwe2.database import Database
3535
from cwe2.database import InvalidCWEError
3636
from packageurl import PackageURL
37+
from packageurl.contrib import purl2url
3738
from packageurl.contrib.django.utils import without_empty_values
3839
from packageurl.contrib.purl2url import purl2url
3940
from packageurl.contrib.url2purl import url2purl
@@ -914,33 +915,35 @@ def generate_commit_url(vcs_url, commit_hash):
914915
if not purl:
915916
return
916917

917-
base_purl = get_core_purl(str(purl))
918-
purl_with_version = PackageURL(
919-
type=base_purl.type, namespace=base_purl.namespace, name=base_purl.name, version=commit_hash
918+
new_purl = PackageURL(
919+
type=purl.type,
920+
namespace=purl.namespace,
921+
name=purl.name,
922+
version=commit_hash,
923+
qualifiers=purl.qualifiers,
920924
)
921-
commit_url = purl2url(str(purl_with_version))
922-
return commit_url
925+
926+
return purl2url.get_commit_url(str(new_purl))
923927

924928

925929
def generate_patch_url(vcs_url, commit_hash):
926930
"""
927931
Generate patch URL from VCS URL and commit hash.
928932
"""
933+
929934
if not vcs_url or not commit_hash:
930-
return None
935+
return
936+
937+
purl = url2purl(vcs_url)
938+
if not purl:
939+
return
940+
941+
new_purl = PackageURL(
942+
type=purl.type,
943+
namespace=purl.namespace,
944+
name=purl.name,
945+
version=commit_hash,
946+
qualifiers=purl.qualifiers,
947+
)
931948

932-
vcs_url = vcs_url.rstrip("/")
933-
934-
if vcs_url.startswith("https://github.com"):
935-
return f"{vcs_url}/commit/{commit_hash}.patch"
936-
elif vcs_url.startswith("https://gitlab.com"):
937-
return f"{vcs_url}/-/commit/{commit_hash}.patch"
938-
elif vcs_url.startswith("https://bitbucket.org"):
939-
return f"{vcs_url}/-/commit/{commit_hash}/raw"
940-
elif vcs_url.startswith("https://codeberg.org"):
941-
return f"{vcs_url}/-/commit/{commit_hash}.patch"
942-
elif vcs_url.startswith("https://android.googlesource.com"):
943-
return f"{vcs_url}/+/{commit_hash}%5E%21?format=TEXT"
944-
elif vcs_url.startswith("https://git.kernel.org"):
945-
return f"{vcs_url}/patch/?id={commit_hash}"
946-
return
949+
return purl2url.get_patch_url(str(new_purl))

vulnerablecode/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def __init__(self, *args, **kwargs):
7575
api_v3_router.register("packages", PackageV3ViewSet, basename="package-v3")
7676

7777
api_v3_router.register(
78-
"package_commit_patches", PackageCommitPatchViewSet, basename="package_commit_patch"
78+
"package-commit-patches", PackageCommitPatchViewSet, basename="package-commit-patch"
7979
)
8080
api_v3_router.register("patches", PatchViewSet, basename="patches")
8181

0 commit comments

Comments
 (0)