|
| 1 | +# Copyright (c) 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 from nexB Inc. and others. |
| 21 | +# Visit https://github.com/nexB/vulnerablecode/ for support and download. |
| 22 | + |
| 23 | +import logging |
| 24 | +import os |
| 25 | +from typing import Iterable |
| 26 | +from typing import List |
| 27 | + |
| 28 | +import yaml |
| 29 | +from dateutil import parser as dateparser |
| 30 | +from fetchcode.vcs import fetch_via_vcs |
| 31 | +from packageurl import PackageURL |
| 32 | +from univers.version_range import RANGE_CLASS_BY_SCHEMES |
| 33 | +from univers.version_range import VersionRange |
| 34 | +from univers.versions import Version |
| 35 | + |
| 36 | +from vulnerabilities.importer import AdvisoryData |
| 37 | +from vulnerabilities.importer import AffectedPackage |
| 38 | +from vulnerabilities.importer import Importer |
| 39 | +from vulnerabilities.importer import Reference |
| 40 | +from vulnerabilities.importers.github import get_reference_id |
| 41 | + |
| 42 | +LOGGER = logging.getLogger(__name__) |
| 43 | + |
| 44 | + |
| 45 | +PURL_TYPE_BY_ECOSYSTEM = { |
| 46 | + "gem": "gem", |
| 47 | + "go": "golang", |
| 48 | + "maven": "maven", |
| 49 | + "npm": "npm", |
| 50 | + "nuget": "nuget", |
| 51 | + "pypi": "pypi", |
| 52 | + "packagist": "composer", |
| 53 | +} |
| 54 | + |
| 55 | + |
| 56 | +def fork_and_get_dir(url): |
| 57 | + return fetch_via_vcs(url).dest_dir |
| 58 | + |
| 59 | + |
| 60 | +class ForkError(Exception): |
| 61 | + pass |
| 62 | + |
| 63 | + |
| 64 | +class GitLabAPIImporter(Importer): |
| 65 | + spdx_license_expression = "MIT" |
| 66 | + license_url = "https://gitlab.com/gitlab-org/advisories-community/-/blob/main/LICENSE" |
| 67 | + gitlab_url = "git+https://gitlab.com/gitlab-org/advisories-community/" |
| 68 | + |
| 69 | + def advisory_data(self) -> Iterable[AdvisoryData]: |
| 70 | + """ |
| 71 | + Return a list of AdvisoryData objects |
| 72 | + """ |
| 73 | + try: |
| 74 | + fork_directory = fork_and_get_dir(self.gitlab_url) |
| 75 | + except Exception as e: |
| 76 | + LOGGER.error(f"Can't fetch url {self.gitlab_url}") |
| 77 | + raise ForkError(e) |
| 78 | + ecosystems = ["nuget", "maven", "gem", "npm", "go", "packagist", "pypi"] |
| 79 | + for ecosystem in ecosystems: |
| 80 | + for file in get_files(os.path.join(fork_directory, ecosystem)): |
| 81 | + yield parse_yaml_file(file) |
| 82 | + |
| 83 | + |
| 84 | +def get_files(dir): |
| 85 | + for root, _, files in os.walk(dir): |
| 86 | + for file in files: |
| 87 | + yield os.path.join(root, file) |
| 88 | + |
| 89 | + |
| 90 | +def extract_references(urls: List[str]) -> Iterable[Reference]: |
| 91 | + """ |
| 92 | + Yield `reference` by iterating over `reference_data` |
| 93 | + >>> list(extract_references(["https://github.com/advisories/GHSA-c9hw-wf7x-jp9j"])) |
| 94 | + [Reference(reference_id='GHSA-c9hw-wf7x-jp9j', url='https://github.com/advisories/GHSA-c9hw-wf7x-jp9j', severities=[])] |
| 95 | + >>> list(extract_references(["https://github.com/advisories/c9hw-wf7x-jp9j"])) |
| 96 | + [Reference(reference_id='', url='https://github.com/advisories/c9hw-wf7x-jp9j', severities=[])] |
| 97 | + """ |
| 98 | + for url in urls: |
| 99 | + if not isinstance(url, str): |
| 100 | + LOGGER.error(f"extract_references: url is not of type `str`: {url}") |
| 101 | + continue |
| 102 | + if "GHSA-" in url.upper(): |
| 103 | + reference = Reference(url=url, reference_id=get_reference_id(url)) |
| 104 | + else: |
| 105 | + reference = Reference(url=url) |
| 106 | + yield reference |
| 107 | + |
| 108 | + |
| 109 | +def not_empty(value): |
| 110 | + return value is not None and value != "" |
| 111 | + |
| 112 | + |
| 113 | +def get_purl(package_slug): |
| 114 | + """ |
| 115 | + Return a PackageURL object from a package slug |
| 116 | + """ |
| 117 | + LOGGER.error(package_slug) |
| 118 | + parts = package_slug.split("/") |
| 119 | + parts = list(filter(not_empty, parts)) |
| 120 | + # if package slug is of the form: |
| 121 | + # "nuget/NuGet.Core" |
| 122 | + if len(parts) == 2: |
| 123 | + type, name = parts |
| 124 | + return PackageURL(type=PURL_TYPE_BY_ECOSYSTEM[type], name=name) |
| 125 | + # if package slug is of the form: |
| 126 | + # "nuget/github/user/abc/NuGet.Core" |
| 127 | + if len(parts) >= 3: |
| 128 | + type = parts[0] |
| 129 | + name = parts[-1] |
| 130 | + namespace = "/".join(parts[1:-1]) |
| 131 | + return PackageURL(type=PURL_TYPE_BY_ECOSYSTEM[type], namespace=namespace, name=name) |
| 132 | + LOGGER.error(f"get_purl: package_slug can not be parsed: {package_slug!r}") |
| 133 | + |
| 134 | + |
| 135 | +def extract_affected_packages( |
| 136 | + affected_version_range: VersionRange, |
| 137 | + fixed_versions: List[str], |
| 138 | + purl: PackageURL, |
| 139 | + version_class: Version, |
| 140 | +) -> Iterable[AffectedPackage]: |
| 141 | + """ |
| 142 | + Yield a list of AffectedPackage objects |
| 143 | + """ |
| 144 | + for fixed_version in fixed_versions or []: |
| 145 | + yield AffectedPackage( |
| 146 | + package=purl, |
| 147 | + fixed_version=version_class(fixed_version), |
| 148 | + affected_version_range=affected_version_range, |
| 149 | + ) |
| 150 | + |
| 151 | + |
| 152 | +def parse_yaml_file(file): |
| 153 | + with open(file, "r") as f: |
| 154 | + yaml_file = yaml.safe_load(f) |
| 155 | + if not isinstance(yaml_file, dict): |
| 156 | + LOGGER.error(f"parse_yaml_file: yaml_file is not of type `dict`: {yaml_file!r}") |
| 157 | + return |
| 158 | + |
| 159 | + # refer to schema here https://gitlab.com/gitlab-org/advisories-community/-/blob/main/ci/schema/schema.json |
| 160 | + aliases = yaml_file.get("identifiers") |
| 161 | + summary = yaml_file.get("title") |
| 162 | + urls = yaml_file.get("urls") |
| 163 | + references = list(extract_references(urls)) |
| 164 | + date_published = dateparser.parse(yaml_file.get("pubdate")) |
| 165 | + purl: PackageURL = get_purl(yaml_file.get("package_slug")) |
| 166 | + fixed_versions = yaml_file.get("fixed_versions") |
| 167 | + vrc: VersionRange = RANGE_CLASS_BY_SCHEMES[purl.type] |
| 168 | + affected_range = yaml_file.get("affected_range") |
| 169 | + affected_version_range = None |
| 170 | + version_class = vrc.version_class |
| 171 | + try: |
| 172 | + affected_version_range = vrc.from_native(affected_range) if affected_range else None |
| 173 | + except Exception: |
| 174 | + LOGGER.error(f"parse_yaml_file: affected_range is not parsable`: {affected_range!r}") |
| 175 | + |
| 176 | + if affected_version_range == NotImplementedError: |
| 177 | + LOGGER.error(f"parse_yaml_file: from_native is not implemented yet for {vrc.__name__}`") |
| 178 | + affected_version_range = None |
| 179 | + |
| 180 | + affected_packages = list( |
| 181 | + extract_affected_packages(affected_version_range, fixed_versions, purl, version_class) |
| 182 | + ) |
| 183 | + |
| 184 | + return AdvisoryData( |
| 185 | + aliases=aliases, |
| 186 | + summary=summary, |
| 187 | + references=references, |
| 188 | + date_published=date_published, |
| 189 | + affected_packages=affected_packages, |
| 190 | + ) |
0 commit comments