Skip to content

Commit

Permalink
Refactor OSV ecosystem mapping
Browse files Browse the repository at this point in the history
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 <ziadhany2016@gmail.com>
  • Loading branch information
ziadhany committed Jan 18, 2024
1 parent 2de88cb commit b7f4670
Show file tree
Hide file tree
Showing 28 changed files with 2,071 additions and 46 deletions.
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ toml==0.10.2
tomli==2.0.1
traitlets==5.1.1
typing_extensions==4.1.1
univers==30.10.0
univers==30.11.0
urllib3==1.26.18
wcwidth==0.2.5
websocket-client==0.59.0
Expand Down
2 changes: 2 additions & 0 deletions vulnerabilities/importers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from vulnerabilities.importers import fireeye
from vulnerabilities.importers import gentoo
from vulnerabilities.importers import github
from vulnerabilities.importers import github_osv
from vulnerabilities.importers import gitlab
from vulnerabilities.importers import istio
from vulnerabilities.importers import mozilla
Expand Down Expand Up @@ -67,6 +68,7 @@
fireeye.FireyeImporter,
apache_kafka.ApacheKafkaImporter,
oss_fuzz.OSSFuzzImporter,
github_osv.GithubOSVImporter,
]

IMPORTERS_REGISTRY = {x.qualified_name: x for x in IMPORTERS_REGISTRY}
56 changes: 56 additions & 0 deletions vulnerabilities/importers/github_osv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# VulnerableCode is a trademark of nexB Inc.
# SPDX-License-Identifier: Apache-2.0
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
# See https://github.com/nexB/vulnerablecode for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#
import json
import logging
from pathlib import Path
from typing import Iterable

from vulnerabilities.importer import AdvisoryData
from vulnerabilities.importer import Importer
from vulnerabilities.importers.osv import parse_advisory_data
from vulnerabilities.utils import get_advisory_url

logger = logging.getLogger(__name__)


class GithubOSVImporter(Importer):
license_url = "https://github.com/github/advisory-database/blob/main/LICENSE.md"
spdx_license_expression = "CC-BY-4.0"
repo_url = "git+https://github.com/github/advisory-database/"
importer_name = "GithubOSV Importer"

def advisory_data(self) -> Iterable[AdvisoryData]:
supported_ecosystems = [
"pypi",
"npm",
"maven",
"golang",
"composer",
"hex",
"gem",
"nuget",
"cargo",
]
try:
self.clone(repo_url=self.repo_url)
base_path = Path(self.vcs_response.dest_dir)
# filter out non-github-reviewed files and only keep the files end-with .json
advisory_dirs = base_path / "advisories/github-reviewed"
for file in advisory_dirs.glob("**/*.json"):
advisory_url = get_advisory_url(
file=file,
base_path=base_path,
url="https://github.com/github/advisory-database/blob/main/",
)
with open(file) as f:
raw_data = json.load(f)
yield parse_advisory_data(raw_data, supported_ecosystems, advisory_url)
finally:
if self.vcs_response:
self.vcs_response.delete()
2 changes: 1 addition & 1 deletion vulnerabilities/importers/oss_fuzz.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def advisory_data(self) -> Iterable[AdvisoryData]:
url="https://github.com/pypa/advisory-database/blob/main/",
)
yield parse_advisory_data(
yaml_data, supported_ecosystem="oss-fuzz", advisory_url=advisory_url
yaml_data, supported_ecosystems=["oss-fuzz"], advisory_url=advisory_url
)
finally:
if self.vcs_response:
Expand Down
107 changes: 72 additions & 35 deletions vulnerabilities/importers/osv.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
from typing import Optional

import dateparser
from cvss.exceptions import CVSS3MalformedError
from packageurl import PackageURL
from univers.version_range import RANGE_CLASS_BY_SCHEMES
from univers.versions import InvalidVersion
from univers.versions import PypiVersion
from univers.versions import SemverVersion
from univers.versions import Version

Expand All @@ -31,9 +31,21 @@

logger = logging.getLogger(__name__)

PURL_TYPE_BY_OSV_ECOSYSTEM = {
"npm": "npm",
"pypi": "pypi",
"maven": "maven",
"nuget": "nuget",
"packagist": "composer",
"rubygems": "gem",
"go": "golang",
"hex": "hex",
"cargo": "cargo",
}


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

for affected_pkg in raw_data.get("affected") or []:
purl = get_affected_purl(affected_pkg=affected_pkg, raw_id=raw_id)
if purl.type != supported_ecosystem:
logger.error(f"Unsupported package type: {purl!r} in OSV: {raw_id!r}")

if not purl or purl.type not in supported_ecosystems:
logger.error(f"Unsupported package type: {affected_pkg!r} in OSV: {raw_id!r}")
continue

affected_version_range = get_affected_version_range(
affected_pkg=affected_pkg,
raw_id=raw_id,
supported_ecosystem=supported_ecosystem,
supported_ecosystem=purl.type,
)

for fixed_range in affected_pkg.get("ranges") or []:
fixed_version = get_fixed_versions(fixed_range=fixed_range, raw_id=raw_id)
fixed_version = get_fixed_versions(
fixed_range=fixed_range, raw_id=raw_id, supported_ecosystem=purl.type
)

for version in fixed_version:
affected_packages.append(
Expand Down Expand Up @@ -121,14 +136,21 @@ def get_severities(raw_data) -> Iterable[VulnerabilitySeverity]:
"""
Yield VulnerabilitySeverity extracted from a mapping of OSV ``raw_data``
"""
for severity in raw_data.get("severity") or []:
if severity.get("type") == "CVSS_V3":
vector = severity["score"]
system = SCORING_SYSTEMS["cvssv3.1"]
score = system.compute(vector)
yield VulnerabilitySeverity(system=system, value=score, scoring_elements=vector)
else:
logger.error(f"Unsupported severity type: {severity!r} for OSV id: {raw_data['id']!r}")
try:
for severity in raw_data.get("severity") or []:
if severity.get("type") == "CVSS_V3":
vector = severity["score"]
valid_vector = vector[::-1] if vector[-1] == "/" else vector
system = SCORING_SYSTEMS["cvssv3.1"]
score = system.compute(valid_vector)
yield VulnerabilitySeverity(system=system, value=score, scoring_elements=vector)

else:
logger.error(
f"Unsupported severity type: {severity!r} for OSV id: {raw_data['id']!r}"
)
except CVSS3MalformedError as e:
logger.error(f"Invalid severity {e}")

ecosystem_specific = raw_data.get("ecosystem_specific") or {}
severity = ecosystem_specific.get("severity")
Expand Down Expand Up @@ -173,21 +195,31 @@ def get_affected_purl(affected_pkg, raw_id):
purl = package.get("purl")
if purl:
try:
return PackageURL.from_string(purl)
purl = PackageURL.from_string(purl)
except ValueError:
logger.error(
f"Invalid PackageURL: {purl!r} for OSV "
f"affected_pkg {affected_pkg} and id: {raw_id}"
)

ecosys = package.get("ecosystem")
name = package.get("name")
if ecosys and name:
return PackageURL(type=ecosys, name=name)

logger.error(
f"No PackageURL possible: {purl!r} for affected_pkg {affected_pkg} for OSV id: {raw_id}"
)
else:
ecosys = package.get("ecosystem")
name = package.get("name")
if ecosys and name:
ecosys = ecosys.lower()
purl_type = PURL_TYPE_BY_OSV_ECOSYSTEM.get(ecosys)
if not purl_type:
return
namespace = ""
if purl_type == "maven":
namespace, _, name = name.partition(":")

purl = PackageURL(type=purl_type, namespace=namespace, name=name)
else:
logger.error(
f"No PackageURL possible: {purl!r} for affected_pkg {affected_pkg} for OSV id: {raw_id}"
)
return
return PackageURL.from_string(str(purl))


def get_affected_version_range(affected_pkg, raw_id, supported_ecosystem):
Expand All @@ -206,18 +238,17 @@ def get_affected_version_range(affected_pkg, raw_id, supported_ecosystem):
)


def get_fixed_versions(fixed_range, raw_id) -> List[Version]:
def get_fixed_versions(fixed_range, raw_id, supported_ecosystem) -> List[Version]:
"""
Return a list of unique fixed univers Versions given a ``fixed_range``
univers VersionRange and a ``raw_id``.
For example::
>>> get_fixed_versions(fixed_range={}, raw_id="GHSA-j3f7-7rmc-6wqj")
>>> get_fixed_versions(fixed_range={}, raw_id="GHSA-j3f7-7rmc-6wqj", supported_ecosystem="pypi",)
[]
>>> get_fixed_versions(
... fixed_range={"type": "ECOSYSTEM", "events": [{"fixed": "1.7.0"}]},
... raw_id="GHSA-j3f7-7rmc-6wqj"
... fixed_range={"type": "ECOSYSTEM", "events": [{"fixed": "1.7.0"}], },
... raw_id="GHSA-j3f7-7rmc-6wqj",
... supported_ecosystem="pypi",
... )
[PypiVersion(string='1.7.0')]
"""
Expand All @@ -228,21 +259,27 @@ def get_fixed_versions(fixed_range, raw_id) -> List[Version]:

fixed_range_type = fixed_range["type"]

for version in extract_fixed_versions(fixed_range):
version_range_class = RANGE_CLASS_BY_SCHEMES.get(supported_ecosystem)
version_class = version_range_class.version_class if version_range_class else None

# FIXME: ECOSYSTEM does not imply PyPI!!!!
for version in extract_fixed_versions(fixed_range):
if fixed_range_type == "ECOSYSTEM":
try:
fixed_versions.append(PypiVersion(version))
if not version_class:
raise InvalidVersion(
f"Unsupported version for ecosystem: {supported_ecosystem}"
)
fixed_versions.append(version_class(version))
except InvalidVersion:
logger.error(f"Invalid PypiVersion: {version!r} for OSV id: {raw_id!r}")
logger.error(
f"Invalid version class: {version_class} - {version!r} for OSV id: {raw_id!r}"
)

elif fixed_range_type == "SEMVER":
try:
fixed_versions.append(SemverVersion(version))
except InvalidVersion:
logger.error(f"Invalid SemverVersion: {version!r} for OSV id: {raw_id!r}")

else:
logger.error(f"Unsupported fixed version type: {version!r} for OSV id: {raw_id!r}")

Expand Down
2 changes: 1 addition & 1 deletion vulnerabilities/importers/pypa.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def advisory_data(self) -> Iterable[AdvisoryData]:
for advisory_url, raw_data in fork_and_get_files(base_path=path):
yield parse_advisory_data(
raw_data=raw_data,
supported_ecosystem="pypi",
supported_ecosystems=["pypi"],
advisory_url=advisory_url,
)
finally:
Expand Down
2 changes: 1 addition & 1 deletion vulnerabilities/importers/pysec.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ def advisory_data(self) -> Iterable[AdvisoryData]:
with zip_file.open(file_name) as f:
vul_info = json.load(f)
yield parse_advisory_data(
raw_data=vul_info, supported_ecosystem="pypi", advisory_url=url
raw_data=vul_info, supported_ecosystems=["pypi"], advisory_url=url
)
1 change: 1 addition & 0 deletions vulnerabilities/improvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
valid_versions.DebianOvalImprover,
valid_versions.UbuntuOvalImprover,
valid_versions.OSSFuzzImprover,
valid_versions.GithubOSVImprover,
vulnerability_status.VulnerabilityStatusImprover,
]

Expand Down
6 changes: 6 additions & 0 deletions vulnerabilities/improvers/valid_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from vulnerabilities.importers.debian_oval import DebianOvalImporter
from vulnerabilities.importers.elixir_security import ElixirSecurityImporter
from vulnerabilities.importers.github import GitHubAPIImporter
from vulnerabilities.importers.github_osv import GithubOSVImporter
from vulnerabilities.importers.gitlab import GitLabAPIImporter
from vulnerabilities.importers.istio import IstioImporter
from vulnerabilities.importers.nginx import NginxImporter
Expand Down Expand Up @@ -460,3 +461,8 @@ class UbuntuOvalImprover(ValidVersionImprover):
class OSSFuzzImprover(ValidVersionImprover):
importer = OSSFuzzImporter
ignorable_versions = []


class GithubOSVImprover(ValidVersionImprover):
importer = GithubOSVImporter
ignorable_versions = []
Loading

0 comments on commit b7f4670

Please sign in to comment.