Skip to content

Commit dee6ea2

Browse files
authored
Add support for all osv ecosystems (#926)
Add a GithubOSVImporter to git_importer parametrize test Refactor OSV ecosystem mapping Fix the test Update univers version and pass nuget test Resolve merge conflict Add a test for golang Fix test by adding cwe to expected files Resolve merge conflict Signed-off-by: ziadhany <[email protected]>
1 parent 61eeb72 commit dee6ea2

28 files changed

+2073
-45
lines changed

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from vulnerabilities.importers import fireeye
1919
from vulnerabilities.importers import gentoo
2020
from vulnerabilities.importers import github
21+
from vulnerabilities.importers import github_osv
2122
from vulnerabilities.importers import gitlab
2223
from vulnerabilities.importers import istio
2324
from vulnerabilities.importers import mozilla
@@ -69,6 +70,7 @@
6970
apache_kafka.ApacheKafkaImporter,
7071
oss_fuzz.OSSFuzzImporter,
7172
ruby.RubyImporter,
73+
github_osv.GithubOSVImporter,
7274
]
7375

7476
IMPORTERS_REGISTRY = {x.qualified_name: x for x in IMPORTERS_REGISTRY}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/nexB/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
import json
10+
import logging
11+
from pathlib import Path
12+
from typing import Iterable
13+
14+
from vulnerabilities.importer import AdvisoryData
15+
from vulnerabilities.importer import Importer
16+
from vulnerabilities.importers.osv import parse_advisory_data
17+
from vulnerabilities.utils import get_advisory_url
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
class GithubOSVImporter(Importer):
23+
license_url = "https://github.com/github/advisory-database/blob/main/LICENSE.md"
24+
spdx_license_expression = "CC-BY-4.0"
25+
repo_url = "git+https://github.com/github/advisory-database/"
26+
importer_name = "GithubOSV Importer"
27+
28+
def advisory_data(self) -> Iterable[AdvisoryData]:
29+
supported_ecosystems = [
30+
"pypi",
31+
"npm",
32+
"maven",
33+
"golang",
34+
"composer",
35+
"hex",
36+
"gem",
37+
"nuget",
38+
"cargo",
39+
]
40+
try:
41+
self.clone(repo_url=self.repo_url)
42+
base_path = Path(self.vcs_response.dest_dir)
43+
# filter out non-github-reviewed files and only keep the files end-with .json
44+
advisory_dirs = base_path / "advisories/github-reviewed"
45+
for file in advisory_dirs.glob("**/*.json"):
46+
advisory_url = get_advisory_url(
47+
file=file,
48+
base_path=base_path,
49+
url="https://github.com/github/advisory-database/blob/main/",
50+
)
51+
with open(file) as f:
52+
raw_data = json.load(f)
53+
yield parse_advisory_data(raw_data, supported_ecosystems, advisory_url)
54+
finally:
55+
if self.vcs_response:
56+
self.vcs_response.delete()

vulnerabilities/importers/oss_fuzz.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def advisory_data(self) -> Iterable[AdvisoryData]:
4040
url="https://github.com/pypa/advisory-database/blob/main/",
4141
)
4242
yield parse_advisory_data(
43-
yaml_data, supported_ecosystem="oss-fuzz", advisory_url=advisory_url
43+
yaml_data, supported_ecosystems=["oss-fuzz"], advisory_url=advisory_url
4444
)
4545
finally:
4646
if self.vcs_response:

vulnerabilities/importers/osv.py

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
from typing import Optional
1414

1515
import dateparser
16+
from cvss.exceptions import CVSS3MalformedError
1617
from packageurl import PackageURL
1718
from univers.version_range import RANGE_CLASS_BY_SCHEMES
1819
from univers.versions import InvalidVersion
19-
from univers.versions import PypiVersion
2020
from univers.versions import SemverVersion
2121
from univers.versions import Version
2222

@@ -31,9 +31,21 @@
3131

3232
logger = logging.getLogger(__name__)
3333

34+
PURL_TYPE_BY_OSV_ECOSYSTEM = {
35+
"npm": "npm",
36+
"pypi": "pypi",
37+
"maven": "maven",
38+
"nuget": "nuget",
39+
"packagist": "composer",
40+
"rubygems": "gem",
41+
"go": "golang",
42+
"hex": "hex",
43+
"cargo": "cargo",
44+
}
45+
3446

3547
def parse_advisory_data(
36-
raw_data: dict, supported_ecosystem, advisory_url: str
48+
raw_data: dict, supported_ecosystems, advisory_url: str
3749
) -> Optional[AdvisoryData]:
3850
"""
3951
Return an AdvisoryData build from a ``raw_data`` mapping of OSV advisory and
@@ -56,18 +68,21 @@ def parse_advisory_data(
5668

5769
for affected_pkg in raw_data.get("affected") or []:
5870
purl = get_affected_purl(affected_pkg=affected_pkg, raw_id=raw_id)
59-
if purl.type != supported_ecosystem:
60-
logger.error(f"Unsupported package type: {purl!r} in OSV: {raw_id!r}")
71+
72+
if not purl or purl.type not in supported_ecosystems:
73+
logger.error(f"Unsupported package type: {affected_pkg!r} in OSV: {raw_id!r}")
6174
continue
6275

6376
affected_version_range = get_affected_version_range(
6477
affected_pkg=affected_pkg,
6578
raw_id=raw_id,
66-
supported_ecosystem=supported_ecosystem,
79+
supported_ecosystem=purl.type,
6780
)
6881

6982
for fixed_range in affected_pkg.get("ranges") or []:
70-
fixed_version = get_fixed_versions(fixed_range=fixed_range, raw_id=raw_id)
83+
fixed_version = get_fixed_versions(
84+
fixed_range=fixed_range, raw_id=raw_id, supported_ecosystem=purl.type
85+
)
7186

7287
for version in fixed_version:
7388
affected_packages.append(
@@ -121,14 +136,22 @@ def get_severities(raw_data) -> Iterable[VulnerabilitySeverity]:
121136
"""
122137
Yield VulnerabilitySeverity extracted from a mapping of OSV ``raw_data``
123138
"""
124-
for severity in raw_data.get("severity") or []:
125-
if severity.get("type") == "CVSS_V3":
126-
vector = severity["score"]
127-
system = SCORING_SYSTEMS["cvssv3.1"]
128-
score = system.compute(vector)
129-
yield VulnerabilitySeverity(system=system, value=score, scoring_elements=vector)
130-
else:
131-
logger.error(f"Unsupported severity type: {severity!r} for OSV id: {raw_data['id']!r}")
139+
try:
140+
for severity in raw_data.get("severity") or []:
141+
if severity.get("type") == "CVSS_V3":
142+
vector = severity.get("score")
143+
# remove the / from the end of the vector if / exist
144+
valid_vector = vector[:-1] if vector and vector[-1] == "/" else vector
145+
system = SCORING_SYSTEMS["cvssv3.1"]
146+
score = system.compute(valid_vector)
147+
yield VulnerabilitySeverity(system=system, value=score, scoring_elements=vector)
148+
149+
else:
150+
logger.error(
151+
f"Unsupported severity type: {severity!r} for OSV id: {raw_data['id']!r}"
152+
)
153+
except CVSS3MalformedError as e:
154+
logger.error(f"Invalid severity {e}")
132155

133156
ecosystem_specific = raw_data.get("ecosystem_specific") or {}
134157
severity = ecosystem_specific.get("severity")
@@ -173,21 +196,31 @@ def get_affected_purl(affected_pkg, raw_id):
173196
purl = package.get("purl")
174197
if purl:
175198
try:
176-
return PackageURL.from_string(purl)
199+
purl = PackageURL.from_string(purl)
177200
except ValueError:
178201
logger.error(
179202
f"Invalid PackageURL: {purl!r} for OSV "
180203
f"affected_pkg {affected_pkg} and id: {raw_id}"
181204
)
182-
183-
ecosys = package.get("ecosystem")
184-
name = package.get("name")
185-
if ecosys and name:
186-
return PackageURL(type=ecosys, name=name)
187-
188-
logger.error(
189-
f"No PackageURL possible: {purl!r} for affected_pkg {affected_pkg} for OSV id: {raw_id}"
190-
)
205+
else:
206+
ecosys = package.get("ecosystem")
207+
name = package.get("name")
208+
if ecosys and name:
209+
ecosys = ecosys.lower()
210+
purl_type = PURL_TYPE_BY_OSV_ECOSYSTEM.get(ecosys)
211+
if not purl_type:
212+
return
213+
namespace = ""
214+
if purl_type == "maven":
215+
namespace, _, name = name.partition(":")
216+
217+
purl = PackageURL(type=purl_type, namespace=namespace, name=name)
218+
else:
219+
logger.error(
220+
f"No PackageURL possible: {purl!r} for affected_pkg {affected_pkg} for OSV id: {raw_id}"
221+
)
222+
return
223+
return PackageURL.from_string(str(purl))
191224

192225

193226
def get_affected_version_range(affected_pkg, raw_id, supported_ecosystem):
@@ -206,18 +239,17 @@ def get_affected_version_range(affected_pkg, raw_id, supported_ecosystem):
206239
)
207240

208241

209-
def get_fixed_versions(fixed_range, raw_id) -> List[Version]:
242+
def get_fixed_versions(fixed_range, raw_id, supported_ecosystem) -> List[Version]:
210243
"""
211244
Return a list of unique fixed univers Versions given a ``fixed_range``
212245
univers VersionRange and a ``raw_id``.
213-
214246
For example::
215-
216-
>>> get_fixed_versions(fixed_range={}, raw_id="GHSA-j3f7-7rmc-6wqj")
247+
>>> get_fixed_versions(fixed_range={}, raw_id="GHSA-j3f7-7rmc-6wqj", supported_ecosystem="pypi",)
217248
[]
218249
>>> get_fixed_versions(
219-
... fixed_range={"type": "ECOSYSTEM", "events": [{"fixed": "1.7.0"}]},
220-
... raw_id="GHSA-j3f7-7rmc-6wqj"
250+
... fixed_range={"type": "ECOSYSTEM", "events": [{"fixed": "1.7.0"}], },
251+
... raw_id="GHSA-j3f7-7rmc-6wqj",
252+
... supported_ecosystem="pypi",
221253
... )
222254
[PypiVersion(string='1.7.0')]
223255
"""
@@ -228,21 +260,27 @@ def get_fixed_versions(fixed_range, raw_id) -> List[Version]:
228260

229261
fixed_range_type = fixed_range["type"]
230262

231-
for version in extract_fixed_versions(fixed_range):
263+
version_range_class = RANGE_CLASS_BY_SCHEMES.get(supported_ecosystem)
264+
version_class = version_range_class.version_class if version_range_class else None
232265

233-
# FIXME: ECOSYSTEM does not imply PyPI!!!!
266+
for version in extract_fixed_versions(fixed_range):
234267
if fixed_range_type == "ECOSYSTEM":
235268
try:
236-
fixed_versions.append(PypiVersion(version))
269+
if not version_class:
270+
raise InvalidVersion(
271+
f"Unsupported version for ecosystem: {supported_ecosystem}"
272+
)
273+
fixed_versions.append(version_class(version))
237274
except InvalidVersion:
238-
logger.error(f"Invalid PypiVersion: {version!r} for OSV id: {raw_id!r}")
275+
logger.error(
276+
f"Invalid version class: {version_class} - {version!r} for OSV id: {raw_id!r}"
277+
)
239278

240279
elif fixed_range_type == "SEMVER":
241280
try:
242281
fixed_versions.append(SemverVersion(version))
243282
except InvalidVersion:
244283
logger.error(f"Invalid SemverVersion: {version!r} for OSV id: {raw_id!r}")
245-
246284
else:
247285
logger.error(f"Unsupported fixed version type: {version!r} for OSV id: {raw_id!r}")
248286

vulnerabilities/importers/pypa.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def advisory_data(self) -> Iterable[AdvisoryData]:
3434
for advisory_url, raw_data in fork_and_get_files(base_path=path):
3535
yield parse_advisory_data(
3636
raw_data=raw_data,
37-
supported_ecosystem="pypi",
37+
supported_ecosystems=["pypi"],
3838
advisory_url=advisory_url,
3939
)
4040
finally:

vulnerabilities/importers/pysec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,5 @@ def advisory_data(self) -> Iterable[AdvisoryData]:
4040
with zip_file.open(file_name) as f:
4141
vul_info = json.load(f)
4242
yield parse_advisory_data(
43-
raw_data=vul_info, supported_ecosystem="pypi", advisory_url=url
43+
raw_data=vul_info, supported_ecosystems=["pypi"], advisory_url=url
4444
)

vulnerabilities/improvers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
valid_versions.UbuntuOvalImprover,
2626
valid_versions.OSSFuzzImprover,
2727
valid_versions.RubyImprover,
28+
valid_versions.GithubOSVImprover,
2829
vulnerability_status.VulnerabilityStatusImprover,
2930
]
3031

vulnerabilities/improvers/valid_versions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from vulnerabilities.importers.debian_oval import DebianOvalImporter
3333
from vulnerabilities.importers.elixir_security import ElixirSecurityImporter
3434
from vulnerabilities.importers.github import GitHubAPIImporter
35+
from vulnerabilities.importers.github_osv import GithubOSVImporter
3536
from vulnerabilities.importers.gitlab import GitLabAPIImporter
3637
from vulnerabilities.importers.istio import IstioImporter
3738
from vulnerabilities.importers.nginx import NginxImporter
@@ -466,3 +467,8 @@ class OSSFuzzImprover(ValidVersionImprover):
466467
class RubyImprover(ValidVersionImprover):
467468
importer = RubyImporter
468469
ignorable_versions = []
470+
471+
472+
class GithubOSVImprover(ValidVersionImprover):
473+
importer = GithubOSVImporter
474+
ignorable_versions = []

0 commit comments

Comments
 (0)