Skip to content

Commit 1ed87cd

Browse files
authored
Merge pull request #723 from TG1999/migrate/debian
Migrate debian importer to importer-improver model
2 parents b53e4f4 + 11aaf31 commit 1ed87cd

File tree

13 files changed

+1030
-165
lines changed

13 files changed

+1030
-165
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ toml==0.10.2
107107
tomli==2.0.1
108108
traitlets==5.1.1
109109
typing_extensions==4.1.1
110-
univers==30.4.0
110+
univers==30.5.1
111111
urllib3==1.26.9
112112
wcwidth==0.2.5
113113
websocket-client==0.59.0

vulnerabilities/helpers.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import logging
2727
import os
2828
import re
29+
from collections import defaultdict
2930
from functools import total_ordering
3031
from typing import List
3132
from typing import Optional
@@ -275,8 +276,19 @@ def dedupe(original: List) -> List:
275276
>>> dedupe(["z","i","a","a","d","d"])
276277
['z', 'i', 'a', 'd']
277278
"""
278-
new_list = []
279-
for i in original:
280-
if i not in new_list:
281-
new_list.append(i)
282-
return new_list
279+
return list(dict.fromkeys(original))
280+
281+
282+
def get_affected_packages_by_patched_package(
283+
affected_packages: List[AffectedPackage],
284+
):
285+
"""
286+
Return a mapping of list of vulnerable purls keyed by
287+
purl which fix those vulnerable package.
288+
"""
289+
affected_packages_by_patched_package = defaultdict(list)
290+
for package in affected_packages:
291+
affected_packages_by_patched_package[package.patched_package].append(
292+
package.vulnerable_package
293+
)
294+
return affected_packages_by_patched_package

vulnerabilities/importer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ def get_fixed_purl(self):
150150
return fixed_purl
151151

152152
@classmethod
153-
def merge(cls, affected_packages: Iterable):
153+
def merge(
154+
cls, affected_packages: Iterable
155+
) -> Tuple[PackageURL, List[VersionRange], List[Version]]:
154156
"""
155157
Return a tuple with all attributes of AffectedPackage as a set
156158
for all values in the given iterable of AffectedPackage
@@ -284,6 +286,7 @@ class Importer:
284286

285287
spdx_license_expression = ""
286288
license_url = ""
289+
notice = ""
287290

288291
def __init__(self):
289292
if not self.spdx_license_expression:

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# VulnerableCode is a free software code scanning tool from nexB Inc. and others.
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222
from vulnerabilities.importers import alpine_linux
23+
from vulnerabilities.importers import debian
2324
from vulnerabilities.importers import github
2425
from vulnerabilities.importers import nginx
2526
from vulnerabilities.importers import nvd
@@ -35,6 +36,7 @@
3536
openssl.OpensslImporter,
3637
redhat.RedhatImporter,
3738
pysec.PyPIImporter,
39+
debian.DebianImporter,
3840
]
3941

4042
IMPORTERS_REGISTRY = {x.qualified_name: x for x in IMPORTERS_REGISTRY}

vulnerabilities/importers/debian.py

Lines changed: 156 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,115 +21,206 @@
2121
# VulnerableCode is a free software tool from nexB Inc. and others.
2222
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2323

24-
import dataclasses
24+
import logging
2525
from typing import Any
26+
from typing import Iterable
2627
from typing import List
2728
from typing import Mapping
28-
from typing import Set
2929

3030
import requests
31-
from dateutil import parser as dateparser
31+
from django.db.models.query import QuerySet
3232
from packageurl import PackageURL
33+
from univers.version_range import DebianVersionRange
34+
from univers.versions import DebianVersion
3335

36+
from vulnerabilities.helpers import AffectedPackage as LegacyAffectedPackage
37+
from vulnerabilities.helpers import dedupe
38+
from vulnerabilities.helpers import get_affected_packages_by_patched_package
39+
from vulnerabilities.helpers import get_item
3440
from vulnerabilities.helpers import nearest_patched_package
3541
from vulnerabilities.importer import AdvisoryData
42+
from vulnerabilities.importer import AffectedPackage
3643
from vulnerabilities.importer import Importer
3744
from vulnerabilities.importer import Reference
45+
from vulnerabilities.importer import UnMergeablePackageError
46+
from vulnerabilities.improver import MAX_CONFIDENCE
47+
from vulnerabilities.improver import Improver
48+
from vulnerabilities.improver import Inference
49+
from vulnerabilities.models import Advisory
3850

51+
logger = logging.getLogger(__name__)
3952

40-
class DebianImporter(Importer):
41-
def __enter__(self):
42-
if self.response_is_new():
43-
self._api_response = self._fetch()
44-
45-
else:
46-
self._api_response = {}
47-
48-
def updated_advisories(self) -> Set[AdvisoryData]:
49-
advisories = []
50-
51-
for pkg_name, records in self._api_response.items():
52-
advisories.extend(self._parse(pkg_name, records))
53-
54-
return self.batch_advisories(advisories)
55-
56-
def _fetch(self) -> Mapping[str, Any]:
57-
return requests.get(self.config.debian_tracker_url).json()
5853

59-
def _parse(self, pkg_name: str, records: Mapping[str, Any]) -> List[AdvisoryData]:
60-
advisories = []
61-
ignored_versions = {"3.8.20-4."}
54+
class DebianImporter(Importer):
6255

56+
spdx_license_expression = "MIT"
57+
license_url = "https://www.debian.org/license"
58+
notice = """
59+
From: Tushar Goel <tgoel@nexb.com>
60+
Date: Thu, May 12, 2022 at 11:42 PM +00:00
61+
Subject: Usage of Debian Security Data in VulnerableCode
62+
To: <team@security.debian.org>
63+
64+
Hey,
65+
66+
We would like to integrate the debian security data in vulnerablecode
67+
[1][2] which is a FOSS db of FOSS vulnerability data. We were not able
68+
to know under which license the debian security data comes. We would
69+
be grateful to have your acknowledgement over usage of the debian
70+
security data in vulnerablecode and have some kind of licensing
71+
declaration from your side.
72+
73+
[1] - https://github.com/nexB/vulnerablecode
74+
[2] - https://github.com/nexB/vulnerablecode/pull/723
75+
"""
76+
77+
api_url = "https://security-tracker.debian.org/tracker/data/json"
78+
79+
def get_response(self):
80+
response = requests.get(self.api_url)
81+
if response.status_code == 200:
82+
return response.json()
83+
raise Exception(
84+
f"Failed to fetch data from {self.api_url!r} with status code: {response.status_code!r}"
85+
)
86+
87+
def advisory_data(self) -> Iterable[AdvisoryData]:
88+
response = self.get_response()
89+
for pkg_name, records in response.items():
90+
yield from self.parse(pkg_name, records)
91+
92+
def parse(self, pkg_name: str, records: Mapping[str, Any]) -> Iterable[AdvisoryData]:
6393
for cve_id, record in records.items():
64-
impacted_purls, resolved_purls = [], []
94+
affected_versions = []
95+
fixed_versions = []
6596
if not cve_id.startswith("CVE"):
97+
logger.error(f"Invalid CVE ID: {cve_id} in {record} in package {pkg_name}")
6698
continue
6799

68100
# vulnerabilities starting with something else may not be public yet
69101
# see for instance https://web.archive.org/web/20201215213725/https://security-tracker.debian.org/tracker/TEMP-0000000-A2EB44
70102
# TODO: this would need to be revisited though to ensure we are not missing out on anything
103+
# https://github.com/nexB/vulnerablecode/issues/730
71104

72-
for release_name, release_record in record["releases"].items():
73-
if not release_record.get("repositories", {}).get(release_name):
74-
continue
75-
76-
version = release_record["repositories"][release_name]
105+
releases = record["releases"].items()
106+
for release_name, release_record in releases:
107+
version = get_item(release_record, "repositories", release_name)
77108

78-
if version in ignored_versions:
109+
if not version:
110+
logger.error(
111+
f"Version not found for {release_name} in {record} in package {pkg_name}"
112+
)
79113
continue
80114

81115
purl = PackageURL(
82116
name=pkg_name,
83117
type="deb",
84118
namespace="debian",
85-
version=version,
86119
qualifiers={"distro": release_name},
87120
)
88121

89122
if release_record.get("status", "") == "resolved":
90-
resolved_purls.append(purl)
123+
fixed_versions.append(version)
91124
else:
92-
impacted_purls.append(purl)
93-
94-
if (
95-
"fixed_version" in release_record
96-
and release_record["fixed_version"] not in ignored_versions
97-
):
98-
resolved_purls.append(
99-
PackageURL(
100-
name=pkg_name,
101-
type="deb",
102-
namespace="debian",
103-
version=release_record["fixed_version"],
104-
qualifiers={"distro": release_name},
105-
)
106-
)
125+
affected_versions.append(version)
126+
127+
if "fixed_version" in release_record:
128+
fixed_versions.append(release_record["fixed_version"])
107129

108130
references = []
109131
debianbug = record.get("debianbug")
110132
if debianbug:
111133
bug_url = f"https://bugs.debian.org/cgi-bin/bugreport.cgi?bug={debianbug}"
112-
references.append(Reference(url=bug_url, reference_id=debianbug))
113-
advisories.append(
114-
AdvisoryData(
115-
vulnerability_id=cve_id,
116-
affected_packages=nearest_patched_package(impacted_purls, resolved_purls),
117-
summary=record.get("description", ""),
118-
references=references,
134+
references.append(Reference(url=bug_url, reference_id=str(debianbug)))
135+
affected_versions = dedupe(affected_versions)
136+
fixed_versions = dedupe(fixed_versions)
137+
if affected_versions:
138+
affected_version_range = DebianVersionRange.from_versions(affected_versions)
139+
else:
140+
affected_version_range = None
141+
affected_packages = []
142+
for fixed_version in fixed_versions:
143+
affected_packages.append(
144+
AffectedPackage(
145+
package=purl,
146+
affected_version_range=affected_version_range,
147+
fixed_version=DebianVersion(fixed_version),
148+
)
119149
)
150+
yield AdvisoryData(
151+
aliases=[cve_id],
152+
summary=record.get("description", ""),
153+
affected_packages=affected_packages,
154+
references=references,
120155
)
121156

122-
return advisories
123157

124-
def response_is_new(self):
158+
class DebianBasicImprover(Improver):
159+
@property
160+
def interesting_advisories(self) -> QuerySet:
161+
return Advisory.objects.filter(created_by=DebianImporter.qualified_name)
162+
163+
def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
125164
"""
126-
Return True if a request response is for new data likely changed or
127-
updated since we last checked.
165+
Yield Inferences for the given advisory data
128166
"""
129-
head = requests.head(self.config.debian_tracker_url)
130-
date_str = head.headers.get("last-modified")
131-
last_modified_date = dateparser.parse(date_str)
132-
if self.config.last_run_date:
133-
return self.config.last_run_date < last_modified_date
167+
if not advisory_data.affected_packages:
168+
return
169+
try:
170+
purl, affected_version_ranges, fixed_versions = AffectedPackage.merge(
171+
advisory_data.affected_packages
172+
)
173+
except UnMergeablePackageError:
174+
logger.error(f"Cannot merge with different purls {advisory_data.affected_packages!r}")
175+
return
176+
177+
pkg_type = purl.type
178+
pkg_namespace = purl.namespace
179+
pkg_name = purl.name
180+
pkg_qualifiers = purl.qualifiers
181+
fixed_purls = [
182+
PackageURL(
183+
type=pkg_type,
184+
namespace=pkg_namespace,
185+
name=pkg_name,
186+
version=str(version),
187+
qualifiers=pkg_qualifiers,
188+
)
189+
for version in fixed_versions
190+
]
191+
if not affected_version_ranges:
192+
for fixed_purl in fixed_purls:
193+
yield Inference.from_advisory_data(
194+
advisory_data, # We are getting all valid versions to get this inference
195+
confidence=MAX_CONFIDENCE,
196+
affected_purls=[],
197+
fixed_purl=fixed_purl,
198+
)
199+
else:
200+
aff_versions = set()
201+
for affected_version_range in affected_version_ranges:
202+
for constraint in affected_version_range.constraints:
203+
aff_versions.add(constraint.version.string)
204+
affected_purls = [
205+
PackageURL(
206+
type=pkg_type,
207+
namespace=pkg_namespace,
208+
name=pkg_name,
209+
version=version,
210+
qualifiers=pkg_qualifiers,
211+
)
212+
for version in aff_versions
213+
]
214+
affected_packages: List[LegacyAffectedPackage] = nearest_patched_package(
215+
vulnerable_packages=affected_purls, resolved_packages=fixed_purls
216+
)
134217

135-
return True
218+
for (fixed_package, affected_packages,) in get_affected_packages_by_patched_package(
219+
affected_packages=affected_packages
220+
).items():
221+
yield Inference.from_advisory_data(
222+
advisory_data,
223+
confidence=MAX_CONFIDENCE, # We are getting all valid versions to get this inference
224+
affected_purls=affected_packages,
225+
fixed_purl=fixed_package,
226+
)

vulnerabilities/importers/github.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from vulnerabilities import helpers
3838
from vulnerabilities import severity_systems
3939
from vulnerabilities.helpers import AffectedPackage as LegacyAffectedPackage
40+
from vulnerabilities.helpers import get_affected_packages_by_patched_package
4041
from vulnerabilities.helpers import get_item
4142
from vulnerabilities.helpers import nearest_patched_package
4243
from vulnerabilities.importer import AdvisoryData
@@ -465,18 +466,10 @@ def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
465466
vulnerable_packages=affected_purls, resolved_packages=unaffected_purls
466467
)
467468

468-
unique_patched_packages_with_affected_packages = {}
469-
for package in affected_packages:
470-
if package.patched_package not in unique_patched_packages_with_affected_packages:
471-
unique_patched_packages_with_affected_packages[package.patched_package] = []
472-
unique_patched_packages_with_affected_packages[package.patched_package].append(
473-
package.vulnerable_package
474-
)
475-
476469
for (
477470
fixed_package,
478471
affected_packages,
479-
) in unique_patched_packages_with_affected_packages.items():
472+
) in get_affected_packages_by_patched_package(affected_packages).items():
480473
yield Inference.from_advisory_data(
481474
advisory_data,
482475
confidence=100, # We are getting all valid versions to get this inference

vulnerabilities/improvers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
default.DefaultImprover,
2929
importers.nginx.NginxBasicImprover,
3030
importers.github.GitHubBasicImprover,
31+
importers.debian.DebianBasicImprover,
3132
]
3233

3334
IMPROVERS_REGISTRY = {x.qualified_name: x for x in IMPROVERS_REGISTRY}

vulnerabilities/templates/vulnerabilities.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ <h1 class="title">
3636
</tr>
3737
{% for vulnerability in vulnerabilities %}
3838
<tr>
39-
<td><a href="{% url 'vulnerability_view' vulnerability.pk %}">{{vulnerability.vulcoid}}</a></td>
39+
<td><a href="{% url 'vulnerability_view' vulnerability.pk %}">{{vulnerability.vulnerability_id}}</a></td>
4040
<td>{{vulnerability.vulnerable_package_count}}</td>
4141
<td>{{vulnerability.patched_package_count}}</td>
4242
</tr>

0 commit comments

Comments
 (0)