Skip to content

Commit 17c386c

Browse files
committed
Extract API logic to utils from Gitlab datasource #1903
* Separate Gitlab API handling logic from vulntotal Gitlab datasource to utils file Signed-off-by: Michael Ehab Mikhail <[email protected]>
1 parent a05b65e commit 17c386c

File tree

4 files changed

+111
-88
lines changed

4 files changed

+111
-88
lines changed

vulntotal/datasources/gitlab.py

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from fetchcode import fetch
2020
from packageurl import PackageURL
2121

22+
from vulntotal.datasources.gitlab_api import fetch_gitlab_advisories_for_purl
23+
from vulntotal.datasources.gitlab_api import fetch_yaml
2224
from vulntotal.validator import DataSource
2325
from vulntotal.validator import VendorData
2426
from vulntotal.vulntotal_utils import gitlab_constraints_satisfied
@@ -40,18 +42,12 @@ def datasource_advisory(self, purl) -> Iterable[VendorData]:
4042
Yields:
4143
VendorData instance containing the advisory information for the package.
4244
"""
43-
package_slug = get_package_slug(purl)
44-
directory_files = fetch_directory_contents(package_slug)
45-
if not directory_files:
46-
path = self.supported_ecosystem()[purl.type]
47-
casesensitive_package_slug = get_casesensitive_slug(path, package_slug)
48-
directory_files = fetch_directory_contents(casesensitive_package_slug)
45+
advisories = fetch_gitlab_advisories_for_purl(
46+
purl, self.supported_ecosystem(), get_casesensitive_slug
47+
)
4948

50-
if directory_files:
51-
yml_files = [file for file in directory_files if file["name"].endswith(".yml")]
52-
53-
interesting_advisories = parse_interesting_advisories(yml_files, purl)
54-
return interesting_advisories
49+
if advisories:
50+
return parse_interesting_advisories(advisories, purl)
5551

5652
@classmethod
5753
def supported_ecosystem(cls):
@@ -67,21 +63,6 @@ def supported_ecosystem(cls):
6763
}
6864

6965

70-
def fetch_directory_contents(package_slug):
71-
url = f"https://gitlab.com/api/v4/projects/12006272/repository/tree?path={package_slug}"
72-
response = requests.get(url)
73-
if response.status_code == 200:
74-
return response.json()
75-
76-
77-
def fetch_yaml(file_path):
78-
response = requests.get(
79-
f"https://gitlab.com/gitlab-org/security-products/gemnasium-db/-/raw/master/{file_path}"
80-
)
81-
if response.status_code == 200:
82-
return response.text
83-
84-
8566
def get_package_slug(purl):
8667
"""
8768
Constructs a package slug from a given purl.
@@ -163,27 +144,25 @@ def get_casesensitive_slug(path, package_slug):
163144
has_next = paginated_tree["pageInfo"]["hasNextPage"]
164145

165146

166-
def parse_interesting_advisories(yml_files, purl) -> Iterable[VendorData]:
147+
def parse_interesting_advisories(advisories, purl) -> Iterable[VendorData]:
167148
"""
168149
Parses advisories from YAML files in a given location that match a given version.
169150
170151
Parameters:
171-
yml_files: An array having the paths of yml files to parse.
152+
advisories: A list of advisory dictionaries fetched from the GitLab API.
172153
purl: PURL for the advisory.
173154
174155
Yields:
175156
VendorData instance containing the advisory information for the package.
176157
"""
177158
version = purl.version
178159

179-
for file in yml_files:
180-
yml_data = fetch_yaml(file["path"])
181-
gitlab_advisory = saneyaml.load(yml_data)
182-
affected_range = gitlab_advisory["affected_range"]
160+
for advisory in advisories:
161+
affected_range = advisory.get("affected_range")
183162
if gitlab_constraints_satisfied(affected_range, version):
184163
yield VendorData(
185164
purl=PackageURL(purl.type, purl.namespace, purl.name),
186-
aliases=gitlab_advisory["identifiers"],
165+
aliases=advisory.get("identifiers", []),
187166
affected_versions=[affected_range],
188-
fixed_versions=gitlab_advisory["fixed_versions"],
167+
fixed_versions=advisory.get("fixed_versions", []),
189168
)

vulntotal/datasources/gitlab_api.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import requests
2+
import saneyaml
3+
4+
5+
def fetch_directory_contents(package_slug):
6+
url = f"https://gitlab.com/api/v4/projects/12006272/repository/tree?path={package_slug}"
7+
response = requests.get(url)
8+
if response.status_code == 200:
9+
return response.json()
10+
return []
11+
12+
13+
def fetch_yaml(file_path):
14+
response = requests.get(
15+
f"https://gitlab.com/gitlab-org/security-products/gemnasium-db/-/raw/master/{file_path}"
16+
)
17+
if response.status_code == 200:
18+
return response.text
19+
return None
20+
21+
22+
def get_package_slug(purl, supported_ecosystem):
23+
if purl.type not in supported_ecosystem:
24+
return
25+
ecosystem = supported_ecosystem[purl.type]
26+
package_name = purl.name
27+
if purl.type in ("maven", "composer", "golang"):
28+
package_name = f"{purl.namespace}/{purl.name}"
29+
return f"{ecosystem}/{package_name}"
30+
31+
32+
def get_directory_yml_files(purl, supported_ecosystem, get_casesensitive_slug):
33+
package_slug = get_package_slug(purl, supported_ecosystem)
34+
directory_files = fetch_directory_contents(package_slug)
35+
if not directory_files:
36+
path = supported_ecosystem[purl.type]
37+
casesensitive_package_slug = get_casesensitive_slug(path, package_slug)
38+
directory_files = fetch_directory_contents(casesensitive_package_slug)
39+
if not directory_files:
40+
return []
41+
return [file for file in directory_files if file["name"].endswith(".yml")]
42+
43+
44+
def fetch_gitlab_advisories_for_purl(purl, supported_ecosystem, get_casesensitive_slug):
45+
yml_files = get_directory_yml_files(purl, supported_ecosystem, get_casesensitive_slug)
46+
47+
advisories = []
48+
for file in yml_files:
49+
yml_data = fetch_yaml(file["path"])
50+
if yml_data:
51+
advisories.append(saneyaml.load(yml_data))
52+
return advisories
53+
54+
55+
def get_estimated_advisories_count(purl, supported_ecosystem, get_casesensitive_slug):
56+
return len(get_directory_yml_files(purl, supported_ecosystem, get_casesensitive_slug))
Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,26 @@
11
[
22
{
3-
"purl": "pkg:generic/namespace/test",
4-
"affected_versions": [
5-
"<=2.7.1"
6-
],
7-
"fixed_versions": [
8-
"2.7.2"
9-
],
10-
"aliases": [
11-
"CVE-2014-1402"
12-
]
3+
"purl": "pkg:pypi/namespace/test",
4+
"affected_versions": ["<=2.7.1"],
5+
"fixed_versions": ["2.7.2"],
6+
"aliases": ["CVE-2014-1402"]
137
},
148
{
15-
"purl": "pkg:generic/namespace/test",
16-
"affected_versions": [
17-
"<2.8.1"
18-
],
19-
"fixed_versions": [
20-
"2.8.1"
21-
],
22-
"aliases": [
23-
"GHSA-hj2j-77xm-mc5v",
24-
"CVE-2016-10745"
25-
]
9+
"purl": "pkg:pypi/namespace/test",
10+
"affected_versions": ["<2.8.1"],
11+
"fixed_versions": ["2.8.1"],
12+
"aliases": ["GHSA-hj2j-77xm-mc5v", "CVE-2016-10745"]
2613
},
2714
{
28-
"purl": "pkg:generic/namespace/test",
29-
"affected_versions": [
30-
"<2.10.1"
31-
],
32-
"fixed_versions": [
33-
"2.10.1"
34-
],
35-
"aliases": [
36-
"CVE-2019-10906"
37-
]
15+
"purl": "pkg:pypi/namespace/test",
16+
"affected_versions": ["<2.10.1"],
17+
"fixed_versions": ["2.10.1"],
18+
"aliases": ["CVE-2019-10906"]
3819
},
3920
{
40-
"purl": "pkg:generic/namespace/test",
41-
"affected_versions": [
42-
"<2.11.3"
43-
],
44-
"fixed_versions": [
45-
"2.11.3"
46-
],
47-
"aliases": [
48-
"CVE-2020-28493"
49-
]
21+
"purl": "pkg:pypi/namespace/test",
22+
"affected_versions": ["<2.11.3"],
23+
"fixed_versions": ["2.11.3"],
24+
"aliases": ["CVE-2020-28493"]
5025
}
51-
]
26+
]

vulntotal/tests/test_gitlab.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from vulnerabilities.tests import util_tests
1616
from vulntotal.datasources import gitlab
17+
from vulntotal.datasources import gitlab_api
1718

1819

1920
class TestGitlab(testcase.FileBasedTesting):
@@ -28,12 +29,26 @@ def test_generate_package_advisory_url(self):
2829
"pkg:composer/bolt/[email protected]",
2930
"pkg:nuget/[email protected]",
3031
]
31-
results = [gitlab.get_package_slug(PackageURL.from_string(purl)) for purl in purls]
32+
supported_ecosystem = gitlab.GitlabDataSource.supported_ecosystem()
33+
results = [
34+
gitlab_api.get_package_slug(PackageURL.from_string(purl), supported_ecosystem)
35+
for purl in purls
36+
]
3237
expected_file = self.get_test_loc("package_advisory_url-expected.json", must_exist=False)
3338
util_tests.check_results_against_json(results, expected_file)
3439

35-
@mock.patch("vulntotal.datasources.gitlab.fetch_yaml")
36-
def test_parse_interesting_advisories(self, mock_fetch_yaml):
40+
@mock.patch("vulntotal.datasources.gitlab_api.fetch_yaml")
41+
@mock.patch("vulntotal.datasources.gitlab_api.fetch_directory_contents")
42+
def test_parse_interesting_advisories(self, mock_fetch_directory_contents, mock_fetch_yaml):
43+
# Mock the directory contents response
44+
mock_fetch_directory_contents.return_value = [
45+
{"name": "CVE-2014-1402.yml", "path": "path/to/CVE-2014-1402.yml"},
46+
{"name": "CVE-2016-10745.yml", "path": "path/to/CVE-2016-10745.yml"},
47+
{"name": "CVE-2019-10906.yml", "path": "path/to/CVE-2019-10906.yml"},
48+
{"name": "CVE-2019-8341.yml", "path": "path/to/CVE-2019-8341.yml"},
49+
{"name": "CVE-2020-28493.yml", "path": "path/to/CVE-2020-28493.yml"},
50+
]
51+
3752
# Mock the yaml file responses
3853
advisory_folder = (
3954
Path(__file__)
@@ -51,17 +66,15 @@ def test_parse_interesting_advisories(self, mock_fetch_yaml):
5166

5267
mock_fetch_yaml.side_effect = yaml_files
5368

54-
purl = PackageURL("generic", "namespace", "test", "0.1.1")
69+
purl = PackageURL("pypi", "namespace", "test", "0.1.1")
5570

56-
yml_files = [
57-
{"name": "CVE-2014-1402.yml", "path": "path/to/CVE-2014-1402.yml"},
58-
{"name": "CVE-2016-10745.yml", "path": "path/to/CVE-2016-10745.yml"},
59-
{"name": "CVE-2019-10906.yml", "path": "path/to/CVE-2019-10906.yml"},
60-
{"name": "CVE-2019-8341.yml", "path": "path/to/CVE-2019-8341.yml"},
61-
{"name": "CVE-2020-28493.yml", "path": "path/to/CVE-2020-28493.yml"},
62-
]
71+
supported_ecosystem = gitlab.GitlabDataSource.supported_ecosystem()
72+
73+
advisories = gitlab_api.fetch_gitlab_advisories_for_purl(
74+
purl, supported_ecosystem, gitlab.get_casesensitive_slug
75+
)
6376

64-
results = [adv.to_dict() for adv in gitlab.parse_interesting_advisories(yml_files, purl)]
77+
results = [adv.to_dict() for adv in gitlab.parse_interesting_advisories(advisories, purl)]
6578

6679
expected_file = self.get_test_loc("parsed_advisory-expected.json", must_exist=False)
6780
util_tests.check_results_against_json(results, expected_file)

0 commit comments

Comments
 (0)