Skip to content

Commit 9381258

Browse files
committed
Add API support for Patch/PackageCommitPatch
Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 94a9c8f commit 9381258

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed

vulnerabilities/api_v2.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,17 @@
3333
from vulnerabilities.models import CodeFixV2
3434
from vulnerabilities.models import ImpactedPackage
3535
from vulnerabilities.models import Package
36+
from vulnerabilities.models import PackageCommitPatch
3637
from vulnerabilities.models import PackageV2
38+
from vulnerabilities.models import Patch
3739
from vulnerabilities.models import PipelineRun
3840
from vulnerabilities.models import PipelineSchedule
3941
from vulnerabilities.models import Vulnerability
4042
from vulnerabilities.models import VulnerabilityReference
4143
from vulnerabilities.models import VulnerabilitySeverity
4244
from vulnerabilities.models import Weakness
4345
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
46+
from vulnerabilities.utils import get_patch_url
4447
from vulnerabilities.utils import group_advisories_by_content
4548

4649

@@ -333,20 +336,49 @@ def get_fixing_vulnerabilities(self, obj):
333336
return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()]
334337

335338

339+
class PackageCommitPatchSerializer(serializers.ModelSerializer):
340+
patch_url = serializers.SerializerMethodField()
341+
342+
class Meta:
343+
model = PackageCommitPatch
344+
fields = [
345+
"id",
346+
"commit_hash",
347+
"vcs_url",
348+
"patch_url",
349+
]
350+
351+
def get_patch_url(self, obj):
352+
return get_patch_url(obj.vcs_url, obj.commit_hash)
353+
354+
355+
class PatchSerializer(serializers.ModelSerializer):
356+
class Meta:
357+
model = Patch
358+
fields = [
359+
"id",
360+
"patch_url",
361+
]
362+
363+
336364
class PackageV3Serializer(serializers.ModelSerializer):
337365
purl = serializers.CharField(source="package_url")
338366
risk_score = serializers.FloatField(read_only=True)
339367
affected_by_vulnerabilities = serializers.SerializerMethodField()
340368
fixing_vulnerabilities = serializers.SerializerMethodField()
341369
next_non_vulnerable_version = serializers.SerializerMethodField()
342370
latest_non_vulnerable_version = serializers.SerializerMethodField()
371+
introduced_by_package_commit_patches = serializers.SerializerMethodField()
372+
fixed_by_package_commit_patches = serializers.SerializerMethodField()
343373

344374
class Meta:
345375
model = Package
346376
fields = [
347377
"purl",
348378
"affected_by_vulnerabilities",
349379
"fixing_vulnerabilities",
380+
"introduced_by_package_commit_patches",
381+
"fixed_by_package_commit_patches",
350382
"next_non_vulnerable_version",
351383
"latest_non_vulnerable_version",
352384
"risk_score",
@@ -425,6 +457,98 @@ def get_fixing_vulnerabilities(self, package):
425457

426458
return result
427459

460+
def get_introduced_by_package_commit_patches(self, package):
461+
impacts = package.affected_in_impacts.select_related("advisory").prefetch_related(
462+
"introduced_by_package_commit_patches"
463+
)
464+
465+
avids = {impact.advisory.avid for impact in impacts if impact.advisory_id}
466+
if not avids:
467+
return []
468+
469+
latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)
470+
advisory_by_avid = {adv.avid: adv for adv in latest_advisories}
471+
impact_by_avid = {}
472+
473+
advisories = []
474+
for impact in impacts:
475+
avid = impact.advisory.avid
476+
advisory = advisory_by_avid.get(avid)
477+
if not advisory:
478+
continue
479+
advisories.append(advisory)
480+
impact_by_avid[avid] = impact
481+
482+
grouped_advisories = group_advisories_by_content(advisories=advisories)
483+
484+
result = []
485+
for advisory_group in grouped_advisories.values():
486+
primary_advisory = advisory_group["primary"]
487+
avid = primary_advisory.avid
488+
impact = impact_by_avid.get(avid)
489+
490+
if not impact:
491+
continue
492+
493+
patches = impact.introduced_by_package_commit_patches.all()
494+
if not patches:
495+
continue
496+
497+
result.append(
498+
{
499+
"advisory_id": primary_advisory.avid,
500+
"duplicate_advisory_ids": [adv.avid for adv in advisory_group["secondary"]],
501+
"commit_patches": [patch.to_dict() for patch in patches],
502+
}
503+
)
504+
505+
return result
506+
507+
def get_fixed_by_package_commit_patches(self, package):
508+
impacts = package.affected_in_impacts.select_related("advisory").prefetch_related(
509+
"fixed_by_package_commit_patches"
510+
)
511+
512+
avids = {impact.advisory.avid for impact in impacts if impact.advisory_id}
513+
if not avids:
514+
return []
515+
516+
latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)
517+
advisory_by_avid = {adv.avid: adv for adv in latest_advisories}
518+
impact_by_avid = {}
519+
520+
advisories = []
521+
for impact in impacts:
522+
avid = impact.advisory.avid
523+
if advisory := advisory_by_avid.get(avid):
524+
advisories.append(advisory)
525+
impact_by_avid[avid] = impact
526+
527+
grouped_advisories = group_advisories_by_content(advisories=advisories)
528+
529+
result = []
530+
for advisory_group in grouped_advisories.values():
531+
primary_advisory = advisory_group["primary"]
532+
impact = impact_by_avid.get(primary_advisory.avid)
533+
534+
if not impact:
535+
continue
536+
537+
# Query the fixing patches instead
538+
patches = impact.fixed_by_package_commit_patches.all()
539+
if not patches:
540+
continue
541+
542+
result.append(
543+
{
544+
"advisory_id": primary_advisory.avid,
545+
"duplicate_advisory_ids": [adv.avid for adv in advisory_group["secondary"]],
546+
"commit_patches": [patch.to_dict() for patch in patches],
547+
}
548+
)
549+
550+
return result
551+
428552
def get_next_non_vulnerable_version(self, package):
429553
if next_non_vulnerable := package.get_non_vulnerable_versions()[0]:
430554
return next_non_vulnerable.version
@@ -889,6 +1013,40 @@ def get_queryset(self):
8891013
return queryset
8901014

8911015

1016+
class PackageCommitPatchViewSet(viewsets.ReadOnlyModelViewSet):
1017+
"""
1018+
API endpoint that allows viewing PackageCommitPatch entries.
1019+
"""
1020+
1021+
queryset = PackageCommitPatch.objects.all()
1022+
serializer_class = PackageCommitPatchSerializer
1023+
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
1024+
1025+
def get_queryset(self):
1026+
queryset = PackageCommitPatch.objects.all()
1027+
pk = self.request.query_params.get("id")
1028+
if pk:
1029+
queryset = queryset.filter(id=pk)
1030+
return queryset
1031+
1032+
1033+
class PatchViewSet(viewsets.ReadOnlyModelViewSet):
1034+
"""
1035+
API endpoint that allows viewing PackageCommitPatch entries.
1036+
"""
1037+
1038+
queryset = Patch.objects.all()
1039+
serializer_class = PatchSerializer
1040+
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
1041+
1042+
def get_queryset(self):
1043+
queryset = Patch.objects.all()
1044+
pk = self.request.query_params.get("id")
1045+
if pk:
1046+
queryset = queryset.filter(id=pk)
1047+
return queryset
1048+
1049+
8921050
class CodeFixV2ViewSet(viewsets.ReadOnlyModelViewSet):
8931051
"""
8941052
API endpoint that allows viewing CodeFix entries.

vulnerabilities/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
from cwe2.database import InvalidCWEError
3636
from packageurl import PackageURL
3737
from packageurl.contrib.django.utils import without_empty_values
38+
from packageurl.contrib.purl2url import purl2url
39+
from packageurl.contrib.url2purl import url2purl
3840
from univers.version_range import RANGE_CLASS_BY_SCHEMES
3941
from univers.version_range import AlpineLinuxVersionRange
4042
from univers.version_range import NginxVersionRange
@@ -888,3 +890,18 @@ def group_advisories_by_content(advisories):
888890
entry["secondary"].add(advisory)
889891

890892
return grouped
893+
894+
895+
def get_patch_url(vcs_url, commit_hash):
896+
"""
897+
Generate patch URL from VCS URL and commit hash.
898+
"""
899+
if vcs_url.startswith("https://github.com"):
900+
return f"{vcs_url}/commit/{commit_hash}.patch"
901+
elif vcs_url.startswith("https://gitlab.com"):
902+
return f"{vcs_url}/-/commit/{commit_hash}.patch"
903+
elif vcs_url.startswith("https://bitbucket.org"):
904+
return f"{vcs_url}/-/commit/{commit_hash}/raw"
905+
elif vcs_url.startswith("https://git.kernel.org"):
906+
return f"{vcs_url}.git/patch/?id={commit_hash}"
907+
return

vulnerablecode/urls.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
from vulnerabilities.api import VulnerabilityViewSet
2323
from vulnerabilities.api_v2 import CodeFixV2ViewSet
2424
from vulnerabilities.api_v2 import CodeFixViewSet
25+
from vulnerabilities.api_v2 import PackageCommitPatchViewSet
2526
from vulnerabilities.api_v2 import PackageV2ViewSet
2627
from vulnerabilities.api_v2 import PackageV3ViewSet
28+
from vulnerabilities.api_v2 import PatchViewSet
2729
from vulnerabilities.api_v2 import PipelineScheduleV2ViewSet
2830
from vulnerabilities.api_v2 import VulnerabilityV2ViewSet
2931
from vulnerabilities.views import AdminLoginView
@@ -71,6 +73,11 @@ def __init__(self, *args, **kwargs):
7173

7274
api_v3_router.register("packages", PackageV3ViewSet, basename="package-v3")
7375

76+
api_v3_router.register(
77+
"package_commit_patches", PackageCommitPatchViewSet, basename="package_commit_patch"
78+
)
79+
api_v3_router.register("patches", PatchViewSet, basename="patches")
80+
7481
urlpatterns = [
7582
path("admin/login/", AdminLoginView.as_view(), name="admin-login"),
7683
path("api/v2/", include(api_v2_router.urls)),

0 commit comments

Comments
 (0)