Skip to content

Commit 227c0df

Browse files
committed
Resolve merge migration conflict
Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 645d9cc commit 227c0df

5 files changed

Lines changed: 356 additions & 0 deletions

File tree

vulnerabilities/api_v3.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from django.db.models import Max
1616
from django.db.models import OuterRef
1717
from django.db.models import Prefetch
18+
from django.db.models import Q
1819
from django_filters import rest_framework as filters
1920
from drf_spectacular.utils import extend_schema
2021
from packageurl import PackageURL
@@ -33,7 +34,9 @@
3334
from vulnerabilities.models import Group
3435
from vulnerabilities.models import GroupedAdvisory
3536
from vulnerabilities.models import ImpactedPackageAffecting
37+
from vulnerabilities.models import PackageCommitPatch
3638
from vulnerabilities.models import PackageV2
39+
from vulnerabilities.models import Patch
3740
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
3841
from vulnerabilities.utils import TYPES_WITH_MULTIPLE_IMPORTERS
3942
from vulnerabilities.utils import get_advisories_from_groups
@@ -386,6 +389,48 @@ def get_latest_non_vulnerable_version(self, package):
386389
return latest_non_vulnerable.version
387390

388391

392+
class PackageCommitPatchSerializer(serializers.ModelSerializer):
393+
introduced_in_advisories = serializers.SerializerMethodField()
394+
fixed_in_advisories = serializers.SerializerMethodField()
395+
396+
class Meta:
397+
model = PackageCommitPatch
398+
fields = [
399+
"commit_hash",
400+
"vcs_url",
401+
"commit_url",
402+
"patch_url",
403+
"introduced_in_advisories",
404+
"fixed_in_advisories",
405+
]
406+
407+
def get_introduced_in_advisories(self, obj):
408+
impacts = obj.introduced_in_impacts.all()
409+
return self.serialize_impacts(impacts)
410+
411+
def get_fixed_in_advisories(self, obj):
412+
impacts = obj.fixed_in_impacts.all()
413+
return self.serialize_impacts(impacts)
414+
415+
@staticmethod
416+
def serialize_impacts(impacts):
417+
unique_pairs = set()
418+
for impact in impacts:
419+
unique_pairs.add((impact.base_purl, impact.advisory.avid))
420+
return [{"purl": base_purl, "avid": avid} for base_purl, avid in unique_pairs]
421+
422+
423+
class PatchSerializer(serializers.ModelSerializer):
424+
in_advisories = serializers.SerializerMethodField()
425+
426+
class Meta:
427+
model = Patch
428+
fields = ["patch_url", "in_advisories"]
429+
430+
def get_in_advisories(self, obj):
431+
return [advisory.avid for advisory in obj.advisories.all()]
432+
433+
389434
class PackageV3ViewSet(viewsets.GenericViewSet):
390435
queryset = PackageV2.objects.all()
391436
serializer_class = PackageV3Serializer
@@ -558,6 +603,67 @@ def get_queryset(self):
558603
return AdvisoryV2.objects.filter(**{self.relation: purl}).latest_per_avid()
559604

560605

606+
class PackageCommitPatchFilter(filters.FilterSet):
607+
advisory_avid = filters.CharFilter(method="filter_by_advisory", label="Advisory ID")
608+
purl = filters.CharFilter(method="filter_by_purl", label="Purl")
609+
commit_hash = filters.CharFilter(lookup_expr="exact", label="Commit Hash")
610+
vcs_url = filters.CharFilter(lookup_expr="icontains", label="VCS URL")
611+
612+
class Meta:
613+
model = PackageCommitPatch
614+
fields = ["advisory_avid", "purl", "commit_hash", "vcs_url"]
615+
616+
def filter_by_advisory(self, queryset, name, value):
617+
return queryset.filter(
618+
Q(introduced_in_impacts__advisory__avid=value)
619+
| Q(fixed_in_impacts__advisory__avid=value)
620+
).distinct()
621+
622+
def filter_by_purl(self, queryset, name, value):
623+
return queryset.filter(
624+
Q(introduced_in_impacts__base_purl__icontains=value)
625+
| Q(fixed_in_impacts__base_purl__icontains=value)
626+
).distinct()
627+
628+
629+
class PackageCommitPatchViewSet(viewsets.ReadOnlyModelViewSet):
630+
"""
631+
API endpoint that allows viewing PackageCommitPatch entries
632+
"""
633+
634+
serializer_class = PackageCommitPatchSerializer
635+
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
636+
filter_backends = [filters.DjangoFilterBackend]
637+
filterset_class = PackageCommitPatchFilter
638+
639+
def get_queryset(self):
640+
return PackageCommitPatch.objects.prefetch_related(
641+
"introduced_in_impacts__advisory", "fixed_in_impacts__advisory"
642+
).order_by("id")
643+
644+
645+
class PatchFilter(filters.FilterSet):
646+
advisory_avid = filters.CharFilter(field_name="advisories__avid", label="Advisory ID")
647+
648+
class Meta:
649+
model = Patch
650+
fields = ["advisory_avid"]
651+
652+
653+
class PatchViewSet(viewsets.ReadOnlyModelViewSet):
654+
"""
655+
API endpoint that allows viewing Patch entries
656+
"""
657+
658+
serializer_class = PatchSerializer
659+
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
660+
filter_backends = [filters.DjangoFilterBackend]
661+
filterset_class = PatchFilter
662+
663+
def get_queryset(self):
664+
return Patch.objects.all()
665+
666+
561667
class FixingAdvisoriesViewSet(PackageAdvisoriesViewSet):
562668
relation = "impacted_packages__fixed_by_packages__package_url"
563669

vulnerabilities/tests/test_api_v2.py

Lines changed: 142 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
@@ -834,3 +837,142 @@ def test_filter_codefix_by_advisory_id_not_found(self):
834837
response = self.client.get(self.url, {"advisory_id": "nonexistent/ADVISORY-ID"})
835838
assert response.status_code == status.HTTP_200_OK
836839
assert response.data["count"] == 0
840+
841+
842+
class PackageCommitPatchList(APITestCase):
843+
def setUp(self):
844+
self.advisory = AdvisoryV2.objects.create(
845+
datasource_id="test_source",
846+
advisory_id="TEST-2025-001",
847+
avid="test_source/TEST-2025-001",
848+
unique_content_id="a" * 64,
849+
url="https://example.com/advisory",
850+
date_collected="2025-07-01T00:00:00Z",
851+
)
852+
853+
self.affected_package = PackageV2.objects.from_purl(purl="pkg:github/torvalds/linux@1.0.0")
854+
self.fixed_package = PackageV2.objects.from_purl(purl="pkg:github/torvalds/linux@1.0.1")
855+
856+
self.pkg_commit_patch1 = PackageCommitPatch.objects.create(
857+
commit_hash="2e1c42391ff2556387b3cb6308b24f6f65619feb",
858+
vcs_url="https://github.com/torvalds/linux",
859+
patch_text="From 2e1c42391ff2556387b3cb6308b24f6f65619feb Mon Sep 17 00:00:00 2001...",
860+
)
861+
862+
self.pkg_commit_patch2 = PackageCommitPatch.objects.create(
863+
commit_hash="99253eb750fda6a644d5188fb26c43bad8d5a745",
864+
vcs_url="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git",
865+
patch_text="From 99253eb750fda6a644d5188fb26c43bad8d5a745 Mon Sep 17 00:00:00 2001...",
866+
)
867+
868+
self.pkg_commit_patch3 = PackageCommitPatch.objects.create(
869+
commit_hash="f043bfc98c193c284e2cd768fefabe18ac2fed9b",
870+
vcs_url="https://github.com/torvalds/linux",
871+
patch_text="From f043bfc98c193c284e2cd768fefabe18ac2fed9b Mon Sep 17 00:00:00 2001...",
872+
)
873+
874+
self.impacted_package1 = ImpactedPackage.objects.create(
875+
base_purl="pkg:github/torvalds/linux",
876+
advisory=self.advisory,
877+
)
878+
879+
self.impacted_package2 = ImpactedPackage.objects.create(
880+
base_purl="pkg:generic/git.kernel.org/pub/scm/linux/kernel",
881+
advisory=self.advisory,
882+
)
883+
884+
self.impacted_package1.fixed_by_package_commit_patches.add(self.pkg_commit_patch1)
885+
self.impacted_package1.introduced_by_package_commit_patches.add(self.pkg_commit_patch3)
886+
self.impacted_package2.fixed_by_package_commit_patches.add(self.pkg_commit_patch2)
887+
888+
self.user = ApiUser.objects.create_api_user(username="e@mail.com")
889+
self.auth = f"Token {self.user.auth_token.key}"
890+
self.client = APIClient(enforce_csrf_checks=True)
891+
self.client.credentials(HTTP_AUTHORIZATION=self.auth)
892+
self.url = reverse("package-commit-patch-list")
893+
894+
def test_package_commit_patches_list(self):
895+
response = self.client.get(self.url)
896+
assert response.status_code == 200
897+
results = response.json().get("results", response.json())
898+
assert len(results) == 3
899+
patch_data = results[0]
900+
assert patch_data["vcs_url"] == self.pkg_commit_patch1.vcs_url
901+
assert patch_data["commit_hash"] == self.pkg_commit_patch1.commit_hash
902+
assert patch_data["fixed_in_advisories"] == [
903+
{"avid": self.advisory.avid, "purl": self.impacted_package1.base_purl}
904+
]
905+
assert patch_data["introduced_in_advisories"] == []
906+
907+
def test_filter_by_commit_hash(self):
908+
response = self.client.get(f"{self.url}?commit_hash={self.pkg_commit_patch1.commit_hash}")
909+
results = response.json().get("results", response.json())
910+
assert len(results) == 1
911+
912+
response = self.client.get(f"{self.url}?commit_hash=test")
913+
results = response.json().get("results", response.json())
914+
assert len(results) == 0
915+
916+
def test_filter_by_vcs_url(self):
917+
response = self.client.get(f"{self.url}?vcs_url={self.pkg_commit_patch1.vcs_url}")
918+
results = response.json().get("results", response.json())
919+
assert len(results) == 2
920+
921+
response = self.client.get(f"{self.url}?vcs_url=test")
922+
results = response.json().get("results", response.json())
923+
assert len(results) == 0
924+
925+
def test_filter_by_advisory_avid(self):
926+
response = self.client.get(f"{self.url}?advisory_avid={self.advisory.avid}")
927+
results = response.json().get("results", response.json())
928+
assert len(results) == 3
929+
930+
response = self.client.get(f"{self.url}?advisory_avid=test_source/DOES-NOT-EXIST")
931+
results = response.json().get("results", response.json())
932+
assert len(results) == 0
933+
934+
def test_filter_by_purl(self):
935+
response = self.client.get(f"{self.url}?purl=pkg:github/torvalds/linux")
936+
results = response.json().get("results", response.json())
937+
assert len(results) == 2
938+
939+
response = self.client.get(f"{self.url}?purl=pkg:github/aboutcode-org")
940+
results = response.json().get("results", response.json())
941+
assert len(results) == 0
942+
943+
944+
class PatchList(APITestCase):
945+
def setUp(self):
946+
self.advisory = AdvisoryV2.objects.create(
947+
datasource_id="test_source",
948+
advisory_id="TEST-2025-001",
949+
avid="test_source/TEST-2025-001",
950+
unique_content_id="a" * 64,
951+
url="https://example.com/advisory",
952+
date_collected="2025-07-01T00:00:00Z",
953+
)
954+
955+
self.patch = Patch.objects.create(
956+
patch_url="https://lore.kernel.org/patchwork/patch/1086060/", patch_text="some text"
957+
)
958+
959+
self.advisory.patches.add(self.patch)
960+
961+
self.user = ApiUser.objects.create_api_user(username="e@mail.com")
962+
self.auth = f"Token {self.user.auth_token.key}"
963+
self.client = APIClient(enforce_csrf_checks=True)
964+
self.client.credentials(HTTP_AUTHORIZATION=self.auth)
965+
self.url = reverse("patches-list")
966+
967+
def test_patch_list(self):
968+
response = self.client.get(self.url)
969+
assert response.status_code == 200
970+
results = response.json().get("results", response.json())
971+
assert len(results) == 1
972+
assert results[0]["patch_url"] == self.patch.patch_url
973+
assert results == [
974+
{
975+
"patch_url": "https://lore.kernel.org/patchwork/patch/1086060/",
976+
"in_advisories": ["test_source/TEST-2025-001"],
977+
}
978+
]

vulnerabilities/utils.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from cwe2.database import InvalidCWEError
3838
from packageurl import PackageURL
3939
from packageurl.contrib.django.utils import without_empty_values
40+
from packageurl.contrib.url2purl import url2purl
4041
from univers.version_range import RANGE_CLASS_BY_SCHEMES
4142
from univers.version_range import AlpineLinuxVersionRange
4243
from univers.version_range import NginxVersionRange
@@ -1033,3 +1034,51 @@ def merge_and_save_grouped_advisories(package, advisories, relation):
10331034
"gem",
10341035
"conan",
10351036
]
1037+
1038+
1039+
def generate_commit_url(vcs_url, commit_hash):
1040+
"""
1041+
Generate commit URL from VCS URL and commit hash.
1042+
"""
1043+
from packageurl.contrib import purl2url
1044+
1045+
if not vcs_url or not commit_hash:
1046+
return
1047+
1048+
purl = url2purl(vcs_url)
1049+
if not purl:
1050+
return
1051+
1052+
new_purl = PackageURL(
1053+
type=purl.type,
1054+
namespace=purl.namespace,
1055+
name=purl.name,
1056+
version=commit_hash,
1057+
qualifiers=purl.qualifiers,
1058+
)
1059+
1060+
return purl2url.get_commit_url(str(new_purl))
1061+
1062+
1063+
def generate_patch_url(vcs_url, commit_hash):
1064+
"""
1065+
Generate patch URL from VCS URL and commit hash.
1066+
"""
1067+
from packageurl.contrib import purl2url
1068+
1069+
if not vcs_url or not commit_hash:
1070+
return
1071+
1072+
purl = url2purl(vcs_url)
1073+
if not purl:
1074+
return
1075+
1076+
new_purl = PackageURL(
1077+
type=purl.type,
1078+
namespace=purl.namespace,
1079+
name=purl.name,
1080+
version=commit_hash,
1081+
qualifiers=purl.qualifiers,
1082+
)
1083+
1084+
return purl2url.get_patch_url(str(new_purl))

0 commit comments

Comments
 (0)