Skip to content

Commit df196c6

Browse files
committed
Add PyPI OSV importer
Reference: #607 Signed-off-by: Ziad <ziadhany2016@gmail.com> add the necessary changes Signed-off-by: Ziad <ziadhany2016@gmail.com> remove aliases de-duplicate Signed-off-by: Ziad <ziadhany2016@gmail.com> reslove conflicts Signed-off-by: Ziad <ziadhany2016@gmail.com> add dateparser to setup.cfg Signed-off-by: Ziad <ziadhany2016@gmail.com> Resolving conflicts Signed-off-by: Ziad <ziadhany2016@gmail.com>
1 parent 57c3cc9 commit df196c6

7 files changed

Lines changed: 1009 additions & 0 deletions

File tree

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,4 @@ wcwidth==0.2.5
113113
websocket-client==0.59.0
114114
yarl==1.7.2
115115
zipp==3.8.0
116+
dateparser==1.1.1

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ install_requires =
7575
lxml>=4.6.4
7676
defusedxml>=0.7.1
7777
Markdown>=3.3.0
78+
dateparser>=1.1.1
7879

7980
# networking
8081
GitPython>=3.1.17

vulnerabilities/helpers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,16 @@ def get_item(object: dict, *attributes):
242242
return None
243243
item = item[attribute]
244244
return item
245+
246+
247+
def dedupe(original: List) -> List:
248+
"""
249+
Remove all duplicate items and return a new list
250+
>>> dedupe(["z","i","a","a","d","d"])
251+
['z', 'i', 'a', 'd']
252+
"""
253+
new_list = []
254+
for i in original:
255+
if i not in new_list:
256+
new_list.append(i)
257+
return new_list

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
from vulnerabilities.importers import nginx
2525
from vulnerabilities.importers import nvd
2626
from vulnerabilities.importers import openssl
27+
from vulnerabilities.importers import pysec
2728

2829
IMPORTERS_REGISTRY = [
2930
nginx.NginxImporter,
3031
alpine_linux.AlpineImporter,
3132
github.GitHubAPIImporter,
3233
nvd.NVDImporter,
3334
openssl.OpensslImporter,
35+
pysec.PyPIImporter,
3436
]
3537

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

vulnerabilities/importers/pysec.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Copyright (c) 2017 nexB Inc. and others. All rights reserved.
2+
# http://nexb.com and https://github.com/nexB/vulnerablecode/
3+
# The VulnerableCode software is licensed under the Apache License version 2.0.
4+
# Data generated with VulnerableCode require an acknowledgment.
5+
#
6+
# You may not use this software except in compliance with the License.
7+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
8+
# Unless required by applicable law or agreed to in writing, software distributed
9+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
# specific language governing permissions and limitations under the License.
12+
#
13+
# When you publish or redistribute any data created with VulnerableCode or any VulnerableCode
14+
# derivative work, you must accompany this data with the following acknowledgment:
15+
#
16+
# Generated with VulnerableCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES
17+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
18+
# VulnerableCode should be considered or used as legal advice. Consult an Attorney
19+
# for any legal advice.
20+
# VulnerableCode is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
22+
import json
23+
import logging
24+
from builtins import set
25+
from io import BytesIO
26+
from typing import Iterable
27+
from zipfile import ZipFile
28+
29+
import requests
30+
from dateparser import parse
31+
from packageurl import PackageURL
32+
from univers.version_range import PypiVersionRange
33+
from univers.versions import InvalidVersion
34+
from univers.versions import PypiVersion
35+
from univers.versions import SemverVersion
36+
37+
from vulnerabilities.helpers import dedupe
38+
from vulnerabilities.importer import AdvisoryData
39+
from vulnerabilities.importer import AffectedPackage
40+
from vulnerabilities.importer import Importer
41+
from vulnerabilities.importer import Reference
42+
from vulnerabilities.importer import VulnerabilitySeverity
43+
from vulnerabilities.severity_systems import SCORING_SYSTEMS
44+
45+
logger = logging.getLogger(__name__)
46+
47+
48+
class PyPIImporter(Importer):
49+
license_url = "https://github.com/pypa/advisory-database/blob/main/LICENSE"
50+
spdx_license_expression = "CC-BY-4.0"
51+
52+
def advisory_data(self) -> Iterable[AdvisoryData]:
53+
"""
54+
1. Fetch the data from osv api
55+
2. unzip the file
56+
3. open the file one by one
57+
4. yield the json file to parse_advisory_data
58+
"""
59+
url = "https://osv-vulnerabilities.storage.googleapis.com/PyPI/all.zip"
60+
response = requests.get(url).content
61+
with ZipFile(BytesIO(response)) as zip_file:
62+
for file_name in zip_file.namelist():
63+
with zip_file.open(file_name) as f:
64+
vul_info = json.loads(f.read())
65+
yield parse_advisory_data(vul_info)
66+
67+
68+
def parse_advisory_data(raw_data: dict) -> AdvisoryData:
69+
summary = raw_data.get("summary") or ""
70+
aliases = get_aliases(raw_data)
71+
date_published = get_published_date(raw_data)
72+
severity = get_severity(raw_data)
73+
references = get_references(raw_data, severity)
74+
75+
affected_packages = []
76+
if not "affected" in raw_data:
77+
logger.error(f"affected_packages not found - {raw_data['id'] !r}")
78+
79+
if "affected" in raw_data:
80+
for affected_pkg in raw_data["affected"]:
81+
purl = get_aff_purl(affected_pkg, raw_data["id"])
82+
if purl.type == "pypi":
83+
affected_version_range = get_aff_version_range(affected_pkg, raw_data["id"])
84+
for fixed_range in affected_pkg.get("ranges", []):
85+
fixed_version = get_fixed_version(fixed_range, raw_data["id"])
86+
87+
for version in fixed_version:
88+
affected_packages.append(
89+
AffectedPackage(
90+
package=purl,
91+
affected_version_range=affected_version_range,
92+
fixed_version=version,
93+
)
94+
)
95+
96+
return AdvisoryData(
97+
aliases=aliases,
98+
summary=summary,
99+
affected_packages=affected_packages,
100+
references=references,
101+
date_published=date_published,
102+
)
103+
104+
105+
def fixed_filter(fixed_range) -> []:
106+
"""
107+
>>> fixed_filter({"type": "SEMVER", "events": [{"introduced": "0"}, {"fixed": "1.6.0"}]})
108+
['1.6.0']
109+
>>> fixed_filter({"type": "ECOSYSTEM","events":[{"introduced": "0"},{"fixed": "1.0.0"},{"fixed": "9.0.0"}]})
110+
['1.0.0', '9.0.0']
111+
"""
112+
filter_fixed = list(filter(lambda x: x.keys() == {"fixed"}, fixed_range["events"]))
113+
list_fixed = [i["fixed"] for i in filter_fixed]
114+
return list_fixed
115+
116+
117+
def get_aliases(raw_data) -> []:
118+
"""
119+
aliases field is optional , id is required and these are all aliases from our perspective
120+
converting list of two fields to a dict then , convert it to a list to make sure a list is unique
121+
>>> get_aliases({"id": "GHSA-j3f7-7rmc-6wqj"})
122+
['GHSA-j3f7-7rmc-6wqj']
123+
>>> get_aliases({"aliases": ["CVE-2021-40831"]})
124+
['CVE-2021-40831']
125+
>>> get_aliases({"aliases": ["CVE-2022-22817", "GHSA-8vj2-vxx3-667w"], "id": "GHSA-j3f7-7rmc-6wqj"})
126+
['CVE-2022-22817', 'GHSA-8vj2-vxx3-667w', 'GHSA-j3f7-7rmc-6wqj']
127+
"""
128+
vulnerability_id = raw_data.get("id")
129+
vulnerability_aliases = raw_data.get("aliases") or []
130+
if vulnerability_id:
131+
vulnerability_aliases.append(vulnerability_id)
132+
return vulnerability_aliases
133+
134+
135+
def get_published_date(raw_data):
136+
if "published" in raw_data:
137+
return parse(raw_data["published"])
138+
else:
139+
logger.warning(f"date_published not found {raw_data['id'] !r}")
140+
141+
142+
def get_severity(raw_data) -> []:
143+
severity = []
144+
if "severity" in raw_data:
145+
for sever_list in raw_data["severity"]:
146+
if "type" in sever_list and sever_list["type"] == "CVSS_V3":
147+
severity.append(
148+
VulnerabilitySeverity(
149+
system=SCORING_SYSTEMS["cvssv3_vector"],
150+
value=sever_list["score"],
151+
)
152+
)
153+
if "ecosystem_specific" in raw_data and "severity" in raw_data["ecosystem_specific"]:
154+
severity.append(
155+
VulnerabilitySeverity(
156+
system=SCORING_SYSTEMS["generic_textual"],
157+
value=raw_data["ecosystem_specific"]["severity"],
158+
)
159+
)
160+
else:
161+
logger.warning(f"severity not found- {raw_data['id']!r}")
162+
163+
return severity
164+
165+
166+
def get_references(raw_data, severity) -> []:
167+
if "references" in raw_data:
168+
return [
169+
Reference(url=ref["url"], severities=severity) for ref in raw_data["references"] if ref
170+
]
171+
else:
172+
return []
173+
174+
175+
def get_aff_purl(affected_pkg, raw_id):
176+
package = affected_pkg["package"]
177+
if "purl" in package:
178+
try:
179+
return PackageURL.from_string(package["purl"])
180+
except ValueError:
181+
logger.error(f"PackageURL ValueError - {raw_id !r} - purl: {package['purl'] !r}")
182+
183+
if "ecosystem" in package and "name" in package:
184+
return PackageURL(type=package["ecosystem"], name=package["name"])
185+
else:
186+
logger.error(f"purl affected_pkg not found - {raw_id !r}")
187+
188+
189+
def get_aff_version_range(affected_pkg, raw_id):
190+
if "versions" in affected_pkg:
191+
try:
192+
return PypiVersionRange(affected_pkg["versions"])
193+
except Exception as e:
194+
logger.error(f"affected_pkg_version_range Error - {raw_id !r} - purl: {e !r}")
195+
else:
196+
logger.error(f"affected_pkg_version_range not found - {raw_id !r} ")
197+
198+
199+
def get_fixed_version(fixed_range, raw_id) -> []:
200+
fixed_version = []
201+
if "type" in fixed_range:
202+
list_fixed = fixed_filter(fixed_range)
203+
for i in list_fixed:
204+
if fixed_range["type"] == "ECOSYSTEM":
205+
try:
206+
fixed_version.append(PypiVersion(i))
207+
except InvalidVersion:
208+
logger.error(f"Invalid Version - PypiVersion - {raw_id !r} - {i !r}")
209+
if fixed_range["type"] == "SEMVER":
210+
try:
211+
fixed_version.append(SemverVersion(i))
212+
except InvalidVersion:
213+
logger.error(f"Invalid Version - SemverVersion - {raw_id !r} - {i !r}")
214+
if fixed_range["type"] == "GIT":
215+
# TODO add GitHubVersion univers fix_version
216+
logger.error(f"NotImplementedError GIT Version - {raw_id !r} - {i !r}")
217+
else:
218+
logger.error(f"Invalid type - {raw_id!r}")
219+
220+
return dedupe(fixed_version)

0 commit comments

Comments
 (0)