Skip to content

Commit 94cda22

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

2 files changed

Lines changed: 185 additions & 45 deletions

File tree

vulnerabilities/importers/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@
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

26-
IMPORTERS_REGISTRY = [nginx.NginxImporter, alpine_linux.AlpineImporter, github.GitHubAPIImporter]
27+
IMPORTERS_REGISTRY = [
28+
nginx.NginxImporter,
29+
alpine_linux.AlpineImporter,
30+
github.GitHubAPIImporter,
31+
gitlab.GitLabAPIImporter,
32+
]
2733

2834
IMPORTERS_REGISTRY = {x.qualified_name: x for x in IMPORTERS_REGISTRY}
Lines changed: 178 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,187 @@
1-
from dataclasses import dataclass
2-
import dataclasses
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
324
import os
25+
from typing import Iterable
426
from typing import List
27+
28+
import yaml
29+
from dateutil import parser as dateparser
530
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+
655

756
def fork_and_get_dir(url):
857
return fetch_via_vcs(url).dest_dir
958

10-
def walk_in_directory(dir):
11-
for root, dirs, files in os.walk(dir):
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):
1286
for file in files:
13-
yield os.path.join(root, file)
14-
15-
# binary search
16-
def binary_search(lst, item):
17-
low = 0
18-
high = len(lst) - 1
19-
20-
while low <= high:
21-
mid = (low + high) // 2
22-
guess = lst[mid]
23-
if guess == item:
24-
return mid
25-
if guess > item:
26-
high = mid - 1
87+
if "git" in root:
88+
yield os.path.join(root, file)
89+
90+
91+
def extract_references(urls: List[str]) -> Iterable[Reference]:
92+
"""
93+
Yield `reference` by iterating over `reference_data`
94+
>>> list(extract_references(["https://github.com/advisories/GHSA-c9hw-wf7x-jp9j"]))
95+
[Reference(reference_id='GHSA-c9hw-wf7x-jp9j', url='https://github.com/advisories/GHSA-c9hw-wf7x-jp9j', severities=[])]
96+
>>> list(extract_references(["https://github.com/advisories/c9hw-wf7x-jp9j"]))
97+
[Reference(reference_id='', url='https://github.com/advisories/c9hw-wf7x-jp9j', severities=[])]
98+
"""
99+
for url in urls:
100+
if not isinstance(url, str):
101+
LOGGER.error(f"extract_references: url is not of type `str`: {url}")
102+
continue
103+
if "GHSA-" in url.upper():
104+
reference = Reference(url=url, reference_id=get_reference_id(url))
27105
else:
28-
low = mid + 1
29-
return None
30-
31-
#bubble sort
32-
def bubble_sort(lst):
33-
for i in range(len(lst) - 1):
34-
for j in range(len(lst) - i - 1):
35-
if lst[j] > lst[j + 1]:
36-
lst[j], lst[j + 1] = lst[j + 1], lst[j]
37-
return lst
38-
39-
@dataclasses.dataclass(order=True)
40-
class Test:
41-
name: str
42-
number: int
43-
grades: List[str] = dataclasses.field(default_factory=list)
44-
45-
def __repr__(self):
46-
return self.name
47-
48-
def to_dict(self):
49-
return {
50-
'name': self.name,
51-
'number': self.number,
52-
'grades': [grade for grade in self.list_of_grades]
53-
}
106+
reference = Reference(url=url)
107+
yield reference
108+
109+
110+
def not_empty(value):
111+
return value is not None and value != ""
112+
113+
114+
def get_purl(package_slug):
115+
"""
116+
Return a PackageURL object from a package slug
117+
"""
118+
LOGGER.error(package_slug)
119+
parts = package_slug.split("/")
120+
parts = list(filter(not_empty, parts))
121+
# if package slug is of the form:
122+
# "nuget/NuGet.Core"
123+
if len(parts) == 2:
124+
type, name = parts
125+
return PackageURL(type=PURL_TYPE_BY_ECOSYSTEM[type], name=name)
126+
# if package slug is of the form:
127+
# "nuget/github/user/abc/NuGet.Core"
128+
if len(parts) >= 3:
129+
type = parts[0]
130+
name = parts[-1]
131+
namespace = "/".join(parts[1:-1])
132+
return PackageURL(type=PURL_TYPE_BY_ECOSYSTEM[type], namespace=namespace, name=name)
133+
LOGGER.error(f"get_purl: package_slug can not be parsed: {package_slug!r}")
134+
135+
136+
def extract_affected_packages(
137+
affected_version_range: VersionRange, fixed_versions: List[str], purl: PackageURL
138+
) -> Iterable[AffectedPackage]:
139+
"""
140+
Yield a list of AffectedPackage objects
141+
"""
142+
for fixed_version in fixed_versions or []:
143+
yield AffectedPackage(
144+
package=purl,
145+
fixed_version=affected_version_range.version_class(fixed_version),
146+
affected_version_range=affected_version_range,
147+
)
148+
149+
150+
def parse_yaml_file(file):
151+
with open(file, "r") as f:
152+
yaml_file = yaml.safe_load(f)
153+
if not isinstance(yaml_file, dict):
154+
LOGGER.error(f"parse_yaml_file: yaml_file is not of type `dict`: {yaml_file!r}")
155+
return
156+
157+
# refer to schema here https://gitlab.com/gitlab-org/advisories-community/-/blob/main/ci/schema/schema.json
158+
aliases = yaml_file.get("identifiers")
159+
summary = yaml_file.get("title")
160+
urls = yaml_file.get("urls")
161+
references = list(extract_references(urls))
162+
date_published = dateparser.parse(yaml_file.get("pubdate"))
163+
purl: PackageURL = get_purl(yaml_file.get("package_slug"))
164+
fixed_versions = yaml_file.get("fixed_versions")
165+
vrc: VersionRange = RANGE_CLASS_BY_SCHEMES[purl.type]
166+
affected_range = yaml_file.get("affected_range")
167+
affected_version_range = None
168+
try:
169+
affected_version_range = vrc.from_native(affected_range) if affected_range else None
170+
except Exception:
171+
LOGGER.error(f"parse_yaml_file: affected_range is not parsable`: {affected_range!r}")
172+
173+
if affected_version_range == NotImplementedError:
174+
LOGGER.error(f"parse_yaml_file: from_native is not implemented yet for {vrc.__name__}`")
175+
affected_version_range = None
176+
177+
affected_packages = list(
178+
extract_affected_packages(affected_version_range, fixed_versions, purl)
179+
)
180+
181+
return AdvisoryData(
182+
aliases=aliases,
183+
summary=summary,
184+
references=references,
185+
date_published=date_published,
186+
affected_packages=affected_packages,
187+
)

0 commit comments

Comments
 (0)