Skip to content

Commit 5041b3f

Browse files
committed
Add gitlab importer
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent f563c64 commit 5041b3f

4 files changed

Lines changed: 195 additions & 1 deletion

File tree

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,10 @@ toml==0.10.2
107107
tomli==2.0.1
108108
traitlets==5.1.1
109109
typing_extensions==4.1.1
110-
univers==30.4.0
110+
univers==30.5.1
111111
urllib3==1.26.9
112112
wcwidth==0.2.5
113113
websocket-client==0.59.0
114114
yarl==1.7.2
115115
zipp==3.8.0
116+
fetchcode==0.1.0

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ install_requires =
8080
GitPython>=3.1.17
8181
aiohttp>=3.7.4.post0
8282
requests>=2.25.1
83+
fetchcode>=0.1.0
8384

8485
[options.extras_require]
8586
dev =

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222
from vulnerabilities.importers import alpine_linux
2323
from vulnerabilities.importers import github
24+
from vulnerabilities.importers import gitlab
2425
from vulnerabilities.importers import nginx
2526
from vulnerabilities.importers import nvd
2627
from vulnerabilities.importers import openssl
@@ -31,6 +32,7 @@
3132
github.GitHubAPIImporter,
3233
nvd.NVDImporter,
3334
openssl.OpensslImporter,
35+
gitlab.GitLabAPIImporter,
3436
]
3537

3638
IMPORTERS_REGISTRY = {x.qualified_name: x for x in IMPORTERS_REGISTRY}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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

Comments
 (0)