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
57 changes: 55 additions & 2 deletions vulnerabilities/api_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,18 +221,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_by_patch": introduced_patches,
"fixed_by_patch": fixed_patches,
}
)

Expand Down Expand Up @@ -266,7 +272,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 +283,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_by_patch": introduced_patches,
"fixed_by_patch": fixed_patches,
}
)

return result

if not advisories:
Expand Down Expand Up @@ -456,6 +464,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 +473,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 +684,48 @@ 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 package_commit_patches dicts
fixed_map: (package_id, advisory_avid) -> list of fixed package_commit_patches dicts
Each package_commit_patches dict contains 'commit_hash' and 'vcs_url'
"""
package_ids = [p.id for p in packages]
if not package_ids:
return {}, {}

impacted_packages_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_patches = defaultdict(list)
fixed_patches = defaultdict(list)

for impacted_pkg_qs in impacted_packages_qs:
pkg_id = impacted_pkg_qs.package_id
impact = impacted_pkg_qs.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_patches[key]:
introduced_patches[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_patches[key]:
fixed_patches[key].append(patch_data)

return introduced_patches, fixed_patches


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 %}
58 changes: 57 additions & 1 deletion vulnerabilities/tests/test_api_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@
# See https://aboutcode.org for more information about nexB OSS projects.
#

from django.test import TestCase
from django.urls import reverse
from packageurl import PackageURL
from rest_framework import status
from rest_framework.test import APIClient
from rest_framework.test import APITestCase
from univers.version_range import PypiVersionRange

from vulnerabilities.api_v3 import get_patches_bulk
from vulnerabilities.importer import AdvisoryDataV2
from vulnerabilities.models import AdvisoryV2
from vulnerabilities.models import ImpactedPackage
from vulnerabilities.models import ImpactedPackageAffecting
from vulnerabilities.models import PackageCommitPatch
from vulnerabilities.models import PackageV2
from vulnerabilities.pipes.advisory import insert_advisory_v2
from vulnerabilities.tests.pipelines import TestLogger
Expand Down Expand Up @@ -66,7 +71,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 Expand Up @@ -244,3 +249,54 @@ def test_get_all_vulnerable_purls(self):
results = response.data["results"]
self.assertEqual(len(results), 100)
self.assertIn("next", response.data)


class PatchesBulkTests(TestCase):
def setUp(self):
self.advisory = AdvisoryV2.objects.create(
avid="AVID-123",
advisory_id="importer1/AVID-123",
datasource_id="importer1",
unique_content_id="c524de2f",
url="https://github.com/aboutcode-org/vulnerablecode",
)
self.package = PackageV2.objects.from_purl(purl="pkg:pypi/sample@1.0.0")
self.impact = ImpactedPackage.objects.create(advisory=self.advisory)
ImpactedPackageAffecting.objects.create(package=self.package, impacted_package=self.impact)

self.intro_patch = PackageCommitPatch.objects.create(
commit_hash="98e516011d6e096e25247b82fc5f196bbeecff10",
vcs_url="https://github.com/aboutcode-org/vulnerablecode",
)
self.fixed_patch = PackageCommitPatch.objects.create(
commit_hash="06580c7f99c6fde7bcf18e30bdcc61f081430957",
vcs_url="https://github.com/aboutcode-org/vulnerablecode",
)

self.impact.introduced_by_package_commit_patches.add(self.intro_patch)
self.impact.fixed_by_package_commit_patches.add(self.fixed_patch)

def test_get_patches_bulk_logic(self):
assert get_patches_bulk([]) == ({}, {})
introduced_map, fixed_map = get_patches_bulk([self.package])
expected_key = (self.package.id, self.advisory.avid)

assert introduced_map[expected_key] == [
{
"commit_hash": "98e516011d6e096e25247b82fc5f196bbeecff10",
"vcs_url": "https://github.com/aboutcode-org/vulnerablecode",
}
]
assert fixed_map[expected_key] == [
{
"commit_hash": "06580c7f99c6fde7bcf18e30bdcc61f081430957",
"vcs_url": "https://github.com/aboutcode-org/vulnerablecode",
}
]

self.impact.introduced_by_package_commit_patches.clear()
self.impact.fixed_by_package_commit_patches.clear()

intro_empty, fixed_empty = get_patches_bulk([self.package])
assert intro_empty == {}
assert fixed_empty == {}
Loading
Loading