Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 102 additions & 2 deletions vulnerabilities/api_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from django.db.models import Max
from django.db.models import OuterRef
from django.db.models import Prefetch
from django.db.models import Q
from django_filters import rest_framework as filters
from drf_spectacular.utils import extend_schema
from packageurl import PackageURL
Expand All @@ -33,7 +34,9 @@
from vulnerabilities.models import Group
from vulnerabilities.models import GroupedAdvisory
from vulnerabilities.models import ImpactedPackageAffecting
from vulnerabilities.models import PackageCommitPatch
from vulnerabilities.models import PackageV2
from vulnerabilities.models import Patch
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
from vulnerabilities.utils import TYPES_WITH_MULTIPLE_IMPORTERS
from vulnerabilities.utils import get_advisories_from_groups
Expand Down Expand Up @@ -221,18 +224,24 @@ def get_affected_by_vulnerabilities(self, package):
"""Return a dictionary with advisory as keys and their details, including fixed_by_packages."""
advisories = self.context["advisory_map"].get(package.id, [])
impact_map = self.context["impact_map"].get(package.id, {})
introduced_patch_map = self.context.get("introduced_patch_map", {})
fixed_patch_map = self.context.get("fixed_patch_map", {})

if advisories:
result = []

for adv in advisories:
fixed = impact_map.get(adv["avid"])
introduced_patches = introduced_patch_map.get((package.id, adv["avid"]), [])
fixed_patches = fixed_patch_map.get((package.id, adv["avid"]), [])
adv.pop("avid", None)

result.append(
{
**adv,
"fixed_by_packages": fixed,
"introduced_in_patch": introduced_patches,
"fixed_in_patch": fixed_patches,
}
)

Expand Down Expand Up @@ -266,7 +275,8 @@ def get_affected_by_vulnerabilities(self, package):
impact = impact_by_avid.get(advisory.avid)
if not impact:
continue

introduced_patches = introduced_patch_map.get((package.id, advisory.avid), [])
fixed_patches = fixed_patch_map.get((package.id, advisory.avid), [])
result.append(
{
"advisory_id": advisory.advisory_id.split("/")[-1],
Expand All @@ -276,9 +286,10 @@ def get_affected_by_vulnerabilities(self, package):
"exploitability": advisory.exploitability,
"risk_score": advisory.risk_score,
"fixed_by_packages": [pkg.purl for pkg in impact.fixed_by_packages.all()],
"introduced_in_patch": introduced_patches,
"fixed_in_patch": fixed_patches,
}
)

return result

if not advisories:
Expand Down Expand Up @@ -386,6 +397,48 @@ def get_latest_non_vulnerable_version(self, package):
return latest_non_vulnerable.version


class PackageCommitPatchSerializer(serializers.ModelSerializer):
introduced_in_advisories = serializers.SerializerMethodField()
fixed_in_advisories = serializers.SerializerMethodField()

class Meta:
model = PackageCommitPatch
fields = [
"commit_hash",
"vcs_url",
"commit_url",
"patch_url",
"introduced_in_advisories",
"fixed_in_advisories",
]

def get_introduced_in_advisories(self, obj):
impacts = obj.introduced_in_impacts.all()
return self.serialize_impacts(impacts)

def get_fixed_in_advisories(self, obj):
impacts = obj.fixed_in_impacts.all()
return self.serialize_impacts(impacts)

@staticmethod
def serialize_impacts(impacts):
unique_pairs = set()
for impact in impacts:
unique_pairs.add((impact.base_purl, impact.advisory.avid))
return [{"purl": base_purl, "avid": avid} for base_purl, avid in unique_pairs]


class PatchSerializer(serializers.ModelSerializer):
in_advisories = serializers.SerializerMethodField()

class Meta:
model = Patch
fields = ["patch_url", "in_advisories"]

def get_in_advisories(self, obj):
return [advisory.avid for advisory in obj.advisories.all()]


class PackageV3ViewSet(viewsets.GenericViewSet):
queryset = PackageV2.objects.all()
serializer_class = PackageV3Serializer
Expand Down Expand Up @@ -456,6 +509,7 @@ def create(self, request, *args, **kwargs):
affected_advisory_map = get_affected_advisories_bulk(page)
fixing_advisory_map = get_fixing_advisories_bulk(page)
impact_map = get_impacts_bulk(page)
introduced_patch_map, fixed_patch_map = get_patches_bulk(page)
serializer = self.get_serializer(
page,
many=True,
Expand All @@ -464,6 +518,8 @@ def create(self, request, *args, **kwargs):
"advisory_map": affected_advisory_map,
"impact_map": impact_map,
"fixing_advisory_map": fixing_advisory_map,
"introduced_patch_map": introduced_patch_map,
"fixed_patch_map": fixed_patch_map,
},
)
return self.get_paginated_response(serializer.data)
Expand Down Expand Up @@ -673,6 +729,50 @@ def get_impacts_bulk(packages):
return impact_map


def get_patches_bulk(packages):
"""
Returns a tuple of two dicts:
- introduced_map: (package_id, advisory_avid) -> list of introduced patch dicts
- fixed_map: (package_id, advisory_avid) -> list of fixed patch dicts
Each patch dict contains 'commit_hash' and 'vcs_url'.
Uses ImpactedPackageAffecting to link packages to impacts, then collects patches
from introduced_by_package_commit_patches and fixed_by_package_commit_patches.
"""
package_ids = [p.id for p in packages]
if not package_ids:
return {}, {}

ipa_qs = (
ImpactedPackageAffecting.objects.filter(package_id__in=package_ids)
.select_related("impacted_package__advisory")
.prefetch_related(
"impacted_package__introduced_by_package_commit_patches",
"impacted_package__fixed_by_package_commit_patches",
)
)

introduced_map = defaultdict(list)
fixed_map = defaultdict(list)

for ipa in ipa_qs:
pkg_id = ipa.package_id
impact = ipa.impacted_package
avid = impact.advisory.avid
key = (pkg_id, avid)

for patch in impact.introduced_by_package_commit_patches.all():
patch_data = {"commit_hash": patch.commit_hash, "vcs_url": patch.vcs_url}
if patch_data not in introduced_map[key]:
introduced_map[key].append(patch_data)

for patch in impact.fixed_by_package_commit_patches.all():
patch_data = {"commit_hash": patch.commit_hash, "vcs_url": patch.vcs_url}
if patch_data not in fixed_map[key]:
fixed_map[key].append(patch_data)

return introduced_map, fixed_map


def get_fixing_advisories_bulk(packages):
package_ids = [p.id for p in packages]

Expand Down
46 changes: 44 additions & 2 deletions vulnerabilities/templates/advisory_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,16 @@
</a>
</li>
{% endif %}


<li data-tab="patch-url">
<a>
<span>
{% with pcp_length=package_commit_patches|length %}
Patches: ({{ advisory.patches.count|add:pcp_length }})
{% endwith %}
</span>
</a>
</li>
<!-- <li data-tab="history">
<a>
<span>
Expand Down Expand Up @@ -184,6 +193,18 @@
</a>
</td>
</tr>
<tr>
<td class="two-col-left"
data-tooltip="Risk expressed as a number ranging from 0 to 10. It is calculated by multiplying
the weighted severity and exploitability values, capped at a maximum of 10.
"
>Introduced and Fixed Package Commit Patches</td>
<td class="two-col-right wrap-strings">
<a href="/advisories/commits/{{ advisory.avid }}">
Package Commit Patches Details
</a>
</td>
</tr>
</tbody>
</table>
<div class="has-text-weight-bold tab-nested-div ml-1 mb-1 mt-6">
Expand Down Expand Up @@ -436,7 +457,6 @@
</tr>
{% endfor %}
</div>


<div class="tab-div content" data-content="epss">
{% if epss_data %}
Expand Down Expand Up @@ -503,6 +523,28 @@
{% endif %}
</div>

<div class="tab-div content" data-content="patch-url">
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
<thead>
<tr>
<th style="width: 250px;"> Patch URL </th>
</tr>
</thead>
{% for patch in patches %}
<tr>
<td class="wrap-strings"><a href="{{ patch.patch_url }}" target="_blank">{{ patch.patch_url }}<i
class="fa fa-external-link fa_link_custom"></i></a></td>
</tr>
{% empty %}
<tr>
<td colspan="2">
There are no known patches.
</td>
</tr>
{% endfor %}
</table>
</div>

<div class="tab-div content" data-content="severities-vectors">
{% for severity_vector in severity_vectors %}
{% if severity_vector.vector.version == '2.0' %}
Expand Down
75 changes: 75 additions & 0 deletions vulnerabilities/templates/advisory_package_commit_details.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{% extends "base.html" %}
{% load humanize %}
{% load widget_tweaks %}
{% load static %}
{% load show_cvss %}
{% load url_filters %}

{% block title %}
VulnerableCode Advisory Package Commit Patch Details - {{ advisoryv2.advisory_id }}
{% endblock %}

{% block content %}

{% if advisoryv2 %}
<section class="section pt-0">
<div class="details-container">
<article class="panel is-info panel-header-only">
<div class="panel-heading py-2 is-size-6">
Introduce and Fixing Package Commit Patch details for Advisory:
<span class="tag is-white custom">
{{ advisoryv2.advisory_id }}
</span>
</div>
</article>

<div id="tab-content">
<table class="table vcio-table width-100-pct mt-2">
<thead>
<tr>
<th style="width: 50%;">Introduced in</th>
<th>Fixed by</th>
</tr>
</thead>
<tbody>
{% for impact in advisoryv2.impacted_packages.all %}
{% for pkg_commit_patch in impact.introduced_by_package_commit_patches.all %}
<tr>
<td>
<a href="{{ pkg_commit_patch.vcs_url }}" target="_self">
{{ pkg_commit_patch.base_purl }}@{{ pkg_commit_patch.commit_hash }}
</a>
</td>
<td></td>
</tr>
{% endfor %}

{% for pkg_commit_patch in impact.fixed_by_package_commit_patches.all %}
<tr>
<td></td>
<td>
<a href="{{ pkg_commit_patch.vcs_url }}" target="_self">
{{ impact.base_purl }}@{{ pkg_commit_patch.commit_hash }}
</a>
</td>
</tr>
{% endfor %}

{% empty %}
<tr>
<td colspan="2">
This vulnerability is not known to affect any package commits.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

</div>
</section>
{% endif %}

<script src="{% static 'js/main.js' %}" crossorigin="anonymous"></script>

{% endblock %}
2 changes: 1 addition & 1 deletion vulnerabilities/tests/test_api_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_packages_post_without_details(self):
def test_packages_post_with_details(self):
url = reverse("package-v3-list")

with self.assertNumQueries(31):
with self.assertNumQueries(34):
response = self.client.post(
url,
data={
Expand Down
Loading