Skip to content

Commit b53e4f4

Browse files
authored
Merge pull request #632 from ziadhany/osv_import
Add PyPI OSV Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
2 parents 360754f + a85c9a4 commit b53e4f4

7 files changed

Lines changed: 1049 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
@@ -267,3 +267,16 @@ def _get_gh_response(gh_token, graphql_query):
267267
endpoint = "https://api.github.com/graphql"
268268
headers = {"Authorization": f"bearer {gh_token}"}
269269
return requests.post(endpoint, headers=headers, json=graphql_query).json()
270+
271+
272+
def dedupe(original: List) -> List:
273+
"""
274+
Remove all duplicate items and return a new list preserving ordering
275+
>>> dedupe(["z","i","a","a","d","d"])
276+
['z', 'i', 'a', 'd']
277+
"""
278+
new_list = []
279+
for i in original:
280+
if i not in new_list:
281+
new_list.append(i)
282+
return new_list

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
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
from vulnerabilities.importers import redhat
2829

2930
IMPORTERS_REGISTRY = [
@@ -33,6 +34,7 @@
3334
nvd.NVDImporter,
3435
openssl.OpensslImporter,
3536
redhat.RedhatImporter,
37+
pysec.PyPIImporter,
3638
]
3739

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

vulnerabilities/importers/pysec.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
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 io import BytesIO
25+
from typing import Iterable
26+
from typing import Optional
27+
from zipfile import ZipFile
28+
29+
import dateparser
30+
import requests
31+
from packageurl import PackageURL
32+
from univers.version_range import InvalidVersionRange
33+
from univers.version_range import PypiVersionRange
34+
from univers.versions import InvalidVersion
35+
from univers.versions import PypiVersion
36+
from univers.versions import SemverVersion
37+
38+
from vulnerabilities.helpers import dedupe
39+
from vulnerabilities.importer import AdvisoryData
40+
from vulnerabilities.importer import AffectedPackage
41+
from vulnerabilities.importer import Importer
42+
from vulnerabilities.importer import Reference
43+
from vulnerabilities.importer import VulnerabilitySeverity
44+
from vulnerabilities.severity_systems import SCORING_SYSTEMS
45+
46+
logger = logging.getLogger(__name__)
47+
48+
49+
class PyPIImporter(Importer):
50+
license_url = "https://github.com/pypa/advisory-database/blob/main/LICENSE"
51+
spdx_license_expression = "CC-BY-4.0"
52+
53+
def advisory_data(self) -> Iterable[AdvisoryData]:
54+
"""
55+
1. Fetch the data from osv api
56+
2. unzip the file
57+
3. open the file one by one
58+
4. yield the json file to parse_advisory_data
59+
"""
60+
url = "https://osv-vulnerabilities.storage.googleapis.com/PyPI/all.zip"
61+
response = requests.get(url).content
62+
with ZipFile(BytesIO(response)) as zip_file:
63+
for file_name in zip_file.namelist():
64+
if not file_name.startswith("PYSEC-"):
65+
logger.error(f"NotImplementedError PyPI package file_name: {file_name}")
66+
else:
67+
with zip_file.open(file_name) as f:
68+
vul_info = json.load(f)
69+
yield parse_advisory_data(vul_info)
70+
71+
72+
def parse_advisory_data(raw_data: dict) -> Optional[AdvisoryData]:
73+
raw_id = raw_data["id"]
74+
summary = raw_data.get("summary") or ""
75+
aliases = get_aliases(raw_data)
76+
date_published = get_published_date(raw_data)
77+
severity = list(get_severities(raw_data))
78+
references = get_references(raw_data, severity)
79+
80+
affected_packages = []
81+
if "affected" not in raw_data:
82+
logger.error(f"affected_packages not found - {raw_id !r}")
83+
return
84+
85+
for affected_pkg in raw_data.get("affected") or []:
86+
purl = get_affected_purl(affected_pkg, raw_id)
87+
if purl.type != "pypi":
88+
logger.error(f"Non PyPI package found in PYSEC advisories: {purl} - from: {raw_id !r}")
89+
continue
90+
91+
affected_version_range = get_affected_version_range(affected_pkg, raw_id)
92+
for fixed_range in affected_pkg.get("ranges", []):
93+
fixed_version = get_fixed_version(fixed_range, raw_id)
94+
95+
for version in fixed_version:
96+
affected_packages.append(
97+
AffectedPackage(
98+
package=purl,
99+
affected_version_range=affected_version_range,
100+
fixed_version=version,
101+
)
102+
)
103+
104+
return AdvisoryData(
105+
aliases=aliases,
106+
summary=summary,
107+
affected_packages=affected_packages,
108+
references=references,
109+
date_published=date_published,
110+
)
111+
112+
113+
def fixed_filter(fixed_range) -> []:
114+
"""
115+
Return a list of fixed version strings given a ``fixed_range`` mapping of OSV data.
116+
>>> list(fixed_filter({"type": "SEMVER", "events": [{"introduced": "0"}, {"fixed": "1.6.0"}]}))
117+
['1.6.0']
118+
>>> list(fixed_filter({"type": "ECOSYSTEM","events":[{"introduced": "0"},{"fixed": "1.0.0"},{"fixed": "9.0.0"}]}))
119+
['1.0.0', '9.0.0']
120+
"""
121+
for event in fixed_range.get("events") or []:
122+
fixed = event.get("fixed")
123+
if fixed:
124+
yield fixed
125+
126+
127+
def get_aliases(raw_data) -> []:
128+
"""
129+
aliases field is optional , id is required and these are all aliases from our perspective
130+
converting list of two fields to a dict then , convert it to a list to make sure a list is unique
131+
>>> get_aliases({"id": "GHSA-j3f7-7rmc-6wqj"})
132+
['GHSA-j3f7-7rmc-6wqj']
133+
>>> get_aliases({"aliases": ["CVE-2021-40831"]})
134+
['CVE-2021-40831']
135+
>>> get_aliases({"aliases": ["CVE-2022-22817", "GHSA-8vj2-vxx3-667w"], "id": "GHSA-j3f7-7rmc-6wqj"})
136+
['CVE-2022-22817', 'GHSA-8vj2-vxx3-667w', 'GHSA-j3f7-7rmc-6wqj']
137+
"""
138+
vulnerability_id = raw_data.get("id")
139+
vulnerability_aliases = raw_data.get("aliases") or []
140+
if vulnerability_id:
141+
vulnerability_aliases.append(vulnerability_id)
142+
return vulnerability_aliases
143+
144+
145+
def get_published_date(raw_data):
146+
published = raw_data.get("published")
147+
return published and dateparser.parse(published)
148+
149+
150+
def get_severities(raw_data) -> []:
151+
for sever_list in raw_data.get("severity") or []:
152+
if sever_list.get("type") == "CVSS_V3":
153+
yield VulnerabilitySeverity(
154+
system=SCORING_SYSTEMS["cvssv3.1_vector"], value=sever_list["score"]
155+
)
156+
else:
157+
logger.error(f"NotImplementedError severity type- {raw_data['id']!r}")
158+
159+
ecosys = raw_data.get("ecosystem_specific") or {}
160+
sever = ecosys.get("severity")
161+
if sever:
162+
yield VulnerabilitySeverity(
163+
system=SCORING_SYSTEMS["generic_textual"],
164+
value=sever,
165+
)
166+
167+
database_specific = raw_data.get("database_specific") or {}
168+
sever = database_specific.get("severity")
169+
if sever:
170+
yield VulnerabilitySeverity(
171+
system=SCORING_SYSTEMS["generic_textual"],
172+
value=sever,
173+
)
174+
175+
176+
def get_references(raw_data, severities) -> []:
177+
references = raw_data.get("references") or []
178+
return [Reference(url=ref["url"], severities=severities) for ref in references if ref]
179+
180+
181+
def get_affected_purl(affected_pkg, raw_id):
182+
package = affected_pkg.get("package") or {}
183+
purl = package.get("purl")
184+
if purl:
185+
try:
186+
return PackageURL.from_string(purl)
187+
except ValueError:
188+
logger.error(f"PackageURL ValueError - {raw_id !r} - purl: {purl !r}")
189+
190+
ecosys = package.get("ecosystem")
191+
name = package.get("name")
192+
if ecosys and name:
193+
return PackageURL(type=ecosys, name=name)
194+
else:
195+
logger.error(f"purl affected_pkg not found - {raw_id !r}")
196+
197+
198+
def get_affected_version_range(affected_pkg, raw_id):
199+
affected_versions = affected_pkg.get("versions")
200+
if affected_versions:
201+
try:
202+
return PypiVersionRange(affected_versions)
203+
except InvalidVersionRange:
204+
logger.error(f"InvalidVersionRange affected_pkg_version_range Error - {raw_id !r} ")
205+
else:
206+
logger.error(f"affected_pkg_version_range not found - {raw_id !r} ")
207+
208+
209+
def get_fixed_version(fixed_range, raw_id) -> []:
210+
"""
211+
Return a list of fixed versions, using fixed_filter we get the list of fixed version strings,
212+
then we pass every element to their univers.versions , then we dedupe the result
213+
>>> get_fixed_version({}, "GHSA-j3f7-7rmc-6wqj")
214+
[]
215+
>>> get_fixed_version({"type": "ECOSYSTEM", "events": [{"fixed": "1.7.0"}]}, "GHSA-j3f7-7rmc-6wqj")
216+
[PypiVersion(string='1.7.0')]
217+
"""
218+
fixed_version = []
219+
if "type" not in fixed_range:
220+
logger.error(f"Invalid type - {raw_id!r}")
221+
else:
222+
list_fixed = fixed_filter(fixed_range)
223+
fixed_range_type = fixed_range["type"]
224+
for i in list_fixed:
225+
if fixed_range_type == "ECOSYSTEM":
226+
try:
227+
fixed_version.append(PypiVersion(i))
228+
except InvalidVersion:
229+
logger.error(f"Invalid Version - PypiVersion - {raw_id !r} - {i !r}")
230+
if fixed_range_type == "SEMVER":
231+
try:
232+
fixed_version.append(SemverVersion(i))
233+
except InvalidVersion:
234+
logger.error(f"Invalid Version - SemverVersion - {raw_id !r} - {i !r}")
235+
if fixed_range_type == "GIT":
236+
# TODO add GitHubVersion univers fix_version
237+
logger.error(f"NotImplementedError GIT Version - {raw_id !r} - {i !r}")
238+
239+
return dedupe(fixed_version)

0 commit comments

Comments
 (0)