Skip to content
Open
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
63 changes: 61 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 @@ -353,6 +361,8 @@ def return_advisories_data(self, package, advisories_qs, advisories):
)

impact_by_avid = {impact.advisory.avid: impact for impact in impacts}
introduced_patch_map = self.context.get("introduced_patch_map", {})
fixed_patch_map = self.context.get("fixed_patch_map", {})

result = []
for advisory in advisories:
Expand All @@ -361,6 +371,8 @@ def return_advisories_data(self, package, advisories_qs, advisories):
if not impact:
continue

introduced_patches = introduced_patch_map.get((package.id, advisory.advisory.avid), [])
fixed_patches = fixed_patch_map.get((package.id, advisory.advisory.avid), [])
result.append(
{
"advisory_id": advisory.identifier,
Expand All @@ -372,6 +384,8 @@ def return_advisories_data(self, package, advisories_qs, advisories):
"fixed_by_packages": list(
set([pkg.purl for pkg in impact.fixed_by_packages.all()])
),
"introduced_by_patch": introduced_patches,
"fixed_by_patch": fixed_patches,
}
)

Expand Down Expand Up @@ -456,6 +470,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 +479,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 +690,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 %}
68 changes: 67 additions & 1 deletion vulnerabilities/tests/test_api_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from univers.version_range import PypiVersionRange

from vulnerabilities.importer import AdvisoryDataV2
from vulnerabilities.importer import AffectedPackageV2
from vulnerabilities.importer import PackageCommitPatchData
from vulnerabilities.models import AdvisoryV2
from vulnerabilities.models import PackageV2
from vulnerabilities.pipes.advisory import insert_advisory_v2
Expand Down Expand Up @@ -66,7 +68,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 +246,67 @@ def test_get_all_vulnerable_purls(self):
results = response.data["results"]
self.assertEqual(len(results), 100)
self.assertIn("next", response.data)


class PackageCommitPatchTests(APITestCase):
def setUp(self):
self.advisory = AdvisoryDataV2(
advisory_id="AVID-123",
aliases=[],
affected_packages=[
AffectedPackageV2(
package=PackageURL(type="pypi", name="sample"),
affected_version_range=PypiVersionRange.from_string("vers:pypi/=1.0.0"),
introduced_by_commit_patches=[
PackageCommitPatchData(
vcs_url="https://github.com/aboutcode-org/sample",
commit_hash="06580c7f99c6fde7bcf18e30bdcc61f081430957",
)
],
fixed_by_commit_patches=[
PackageCommitPatchData(
vcs_url="https://github.com/aboutcode-org/sample",
commit_hash="98e516011d6e096e25247b82fc5f196bbeecff10",
)
],
)
],
url="https://github.com/aboutcode-org/sample",
)

self.advisory = insert_advisory_v2(self.advisory, "importer_1", print, 100)
self.client = APIClient(enforce_csrf_checks=True)

def test_packages_commit_patch(self):
url = reverse("package-v3-list")
response = self.client.post(
url,
data={"purls": ["pkg:pypi/sample@1.0.0"], "details": True},
format="json",
)

assert response.status_code == 200
results = response.data["results"]
assert len(results), 1
pkg = results[0]
assert pkg["purl"] == "pkg:pypi/sample@1.0.0"

vulns = pkg.get("affected_by_vulnerabilities", [])
assert len(vulns) == 1
advisory_data = vulns[0]

assert advisory_data["advisory_id"] == "AVID-123"

assert advisory_data["introduced_by_patch"] == [
{
"commit_hash": "06580c7f99c6fde7bcf18e30bdcc61f081430957",
"vcs_url": "https://github.com/aboutcode-org/sample",
}
]

assert advisory_data["fixed_by_patch"] == [
{
"commit_hash": "98e516011d6e096e25247b82fc5f196bbeecff10",
"vcs_url": "https://github.com/aboutcode-org/sample",
}
]
Loading