3030from typing import Optional
3131
3232import pytz
33- import yaml
33+ import saneyaml
3434from dateutil import parser as dateparser
3535from django .db .models .query import QuerySet
3636from fetchcode .vcs import fetch_via_vcs
4141from univers .versions import Version
4242
4343from vulnerabilities .helpers import AffectedPackage as LegacyAffectedPackage
44+ from vulnerabilities .helpers import get_affected_packages_by_patched_package
4445from vulnerabilities .helpers import nearest_patched_package
4546from vulnerabilities .helpers import resolve_version_range
4647from vulnerabilities .importer import AdvisoryData
6768 "nuget" : "nuget" ,
6869 "pypi" : "pypi" ,
6970 "packagist" : "composer" ,
71+ # "conan": "conan",
7072}
7173
7274
@@ -91,32 +93,27 @@ class GitLabAPIImporter(Importer):
9193
9294 def advisory_data (self ) -> Iterable [AdvisoryData ]:
9395 try :
94- fork_directory = fork_and_get_dir (self .gitlab_url )
96+ fork_directory = fork_and_get_dir (url = self .gitlab_url )
9597 except Exception as e :
9698 logger .error (f"Can't clone url { self .gitlab_url } " )
9799 raise ForkError (self .gitlab_url ) from e
98- ecosystems = ["nuget" , "maven" , "gem" , "npm" , "go" , "packagist" , "pypi" ]
99- for ecosystem in ecosystems :
100- for file in get_files (os .path .join (fork_directory , ecosystem )):
101- yield parse_yaml_file (file )
102-
103-
104- def get_files (dir ):
105- for root , _ , files in os .walk (dir ):
106- for file in files :
107- yield os .path .join (root , file )
108-
109-
110- def not_empty (value ):
111- return value is not None and value != ""
100+ for root_dir in os .listdir (fork_directory ):
101+ # skip well known files and directories that contain no advisory data
102+ if root_dir in ("ci" , "CODEOWNERS" , "README.md" , "LICENSE" , ".git" ):
103+ continue
104+ if root_dir not in PURL_TYPE_BY_GITLAB_SCHEME :
105+ logger .error (f"Unknown package type: { root_dir } " )
106+ continue
107+ for root , _ , files in os .walk (os .path .join (fork_directory , root_dir )):
108+ for file in files :
109+ yield parse_gitlab_advisory (file = os .path .join (root , file ))
112110
113111
114112def get_purl (package_slug ):
115113 """
116114 Return a PackageURL object from a package slug
117115 """
118- parts = package_slug .split ("/" )
119- parts = list (filter (not_empty , parts ))
116+ parts = [p for p in package_slug .strip ("/" ).split ("/" ) if p ]
120117 gitlab_scheme = parts [0 ]
121118 purl_type = PURL_TYPE_BY_GITLAB_SCHEME [gitlab_scheme ]
122119 if gitlab_scheme == "go" :
@@ -130,12 +127,11 @@ def get_purl(package_slug):
130127 # if package slug is of the form:
131128 # "nuget/github/user/abc/NuGet.Core"
132129 if len (parts ) >= 3 :
133- gitlab_scheme = parts [0 ]
134130 name = parts [- 1 ]
135131 namespace = "/" .join (parts [1 :- 1 ])
136132 return PackageURL (type = purl_type , namespace = namespace , name = name )
137133 logger .error (f"get_purl: package_slug can not be parsed: { package_slug !r} " )
138- return None
134+ return
139135
140136
141137def extract_affected_packages (
@@ -144,7 +140,12 @@ def extract_affected_packages(
144140 purl : PackageURL ,
145141) -> Iterable [AffectedPackage ]:
146142 """
147- Yield a list of AffectedPackage objects
143+ Yield AffectedPackage objects, one for each fixed_version
144+
145+ In case of gitlab advisory data we get a list of fixed_versions and a affected_version_range.
146+ Since we can not determine which package fixes which range.
147+ We store the all the fixed_versions with the same affected_version_range in the advisory.
148+ Later the advisory data is used to be infered in the GitLabBasicImprover.
148149 """
149150 for fixed_version in fixed_versions :
150151 yield AffectedPackage (
@@ -154,10 +155,11 @@ def extract_affected_packages(
154155 )
155156
156157
157- def parse_yaml_file (file ):
158+ def parse_gitlab_advisory (file ):
158159 """
159- Take file name as input and parse the yaml file
160- to get AdvisoryData object
160+ Parse a Gitlab advisory file and return an AdvisoryData or None.
161+ These files are YAML. There is a JSON schema documented at
162+ https://gitlab.com/gitlab-org/advisories-community/-/blob/main/ci/schema/schema.json
161163
162164 Sample YAML file:
163165 ---
@@ -178,7 +180,7 @@ def parse_yaml_file(file):
178180 - "GMS-2018-26"
179181 """
180182 with open (file , "r" ) as f :
181- gitlab_advisory = yaml . safe_load (f )
183+ gitlab_advisory = saneyaml . load (f )
182184 if not isinstance (gitlab_advisory , dict ):
183185 logger .error (f"parse_yaml_file: yaml_file is not of type `dict`: { gitlab_advisory !r} " )
184186 return
@@ -190,46 +192,52 @@ def parse_yaml_file(file):
190192 references = [Reference .from_url (u ) for u in urls ]
191193 date_published = dateparser .parse (gitlab_advisory .get ("pubdate" ))
192194 date_published = pytz .utc .localize (date_published )
193- fixed_versions = gitlab_advisory .get ("fixed_versions" )
194- affected_version_range = None
195- affected_range = gitlab_advisory .get ("affected_range" )
196- purl : PackageURL = get_purl (gitlab_advisory .get ("package_slug" ))
195+ package_slug = gitlab_advisory .get ("package_slug" )
196+ purl : PackageURL = get_purl ( package_slug = package_slug )
197197 if not purl :
198- logger .error (f"parse_yaml_file: purl is not valid: { file !r} " )
198+ logger .error (f"parse_yaml_file: purl is not valid: { file !r} { package_slug !r } " )
199199 return AdvisoryData (
200200 aliases = aliases ,
201201 summary = summary ,
202202 references = references ,
203203 date_published = date_published ,
204204 )
205+ affected_version_range = None
206+ fixed_versions = gitlab_advisory .get ("fixed_versions" ) or []
207+ affected_range = gitlab_advisory .get ("affected_range" )
208+ gitlab_native_schemes = set (["pypi" , "gem" , "npm" , "go" , "packagist" ])
205209 vrc : VersionRange = RANGE_CLASS_BY_SCHEMES [purl .type ]
206- version_class = vrc .version_class
207- gitlab_native_schemes = ["pypi" , "gem" , "npm" , "go" , "packagist" ]
208210 gitlab_scheme = GITLAB_SCHEME_BY_PURL_TYPE [purl .type ]
209211 try :
210- if gitlab_scheme in gitlab_native_schemes :
211- if affected_range :
212+ if affected_range :
213+ if gitlab_scheme in gitlab_native_schemes :
212214 affected_version_range = from_gitlab_native (
213215 gitlab_scheme = gitlab_scheme , string = affected_range
214216 )
215- else :
216- affected_version_range = vrc .from_native (affected_range ) if affected_range else None
217+ else :
218+ affected_version_range = vrc .from_native (affected_range )
217219 except Exception as e :
218220 logger .error (
219- f"parse_yaml_file: affected_range is not parsable: { affected_range !r} type:{ purl .type } { e } { traceback .format_exc ()} "
221+ f"parse_yaml_file: affected_range is not parsable: { affected_range !r} type:{ purl .type !r } error: { e !r } \n { traceback .format_exc ()} "
220222 )
221223
222224 parsed_fixed_versions = []
223- for fixed_version in fixed_versions or [] :
225+ for fixed_version in fixed_versions :
224226 try :
225- fixed_version = version_class (fixed_version )
227+ fixed_version = vrc . version_class (fixed_version )
226228 parsed_fixed_versions .append (fixed_version )
227229 except Exception as e :
228- logger .error (f"parse_yaml_file: fixed_version is not parsable`: { fixed_version !r} " )
230+ logger .error (
231+ f"parse_yaml_file: fixed_version is not parsable`: { fixed_version !r} error: { e !r} \n { traceback .format_exc ()} "
232+ )
229233
230234 if parsed_fixed_versions :
231235 affected_packages = list (
232- extract_affected_packages (affected_version_range , parsed_fixed_versions , purl )
236+ extract_affected_packages (
237+ affected_version_range = affected_version_range ,
238+ fixed_versions = parsed_fixed_versions ,
239+ purl = purl ,
240+ )
233241 )
234242 else :
235243 if not affected_version_range :
@@ -251,6 +259,14 @@ def parse_yaml_file(file):
251259
252260
253261class GitLabBasicImprover (Improver ):
262+ """
263+ Get the nearest fixed_version and then resolve the version range with the help of all valid versions.
264+ Generate inference between all the affected packages and the fixed_version that fixes all those affected packages.
265+
266+ In case of gitlab advisory data we get a list of fixed_versions and a affected_version_range.
267+ Since we can not determine which package fixes which range.
268+ """
269+
254270 def __init__ (self ) -> None :
255271 self .versions_fetcher_by_purl : Mapping [str , VersionAPI ] = {}
256272
@@ -264,7 +280,7 @@ def get_package_versions(
264280 """
265281 Return a list of `valid_versions` for the `package_url`
266282 """
267- api_name = get_api_package_name (package_url )
283+ api_name = get_api_package_name (purl = package_url )
268284 if not api_name :
269285 logger .error (f"Could not get versions for { package_url !r} " )
270286 return []
@@ -327,21 +343,13 @@ def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
327343 vulnerable_packages = affected_purls , resolved_packages = unaffected_purls
328344 )
329345
330- unique_patched_packages_with_affected_packages = {}
331- for package in affected_packages :
332- if package .patched_package not in unique_patched_packages_with_affected_packages :
333- unique_patched_packages_with_affected_packages [package .patched_package ] = []
334- unique_patched_packages_with_affected_packages [package .patched_package ].append (
335- package .vulnerable_package
336- )
337-
338346 for (
339347 fixed_package ,
340348 affected_packages ,
341- ) in unique_patched_packages_with_affected_packages .items ():
349+ ) in get_affected_packages_by_patched_package ( affected_packages ) .items ():
342350 yield Inference .from_advisory_data (
343- advisory_data ,
344- confidence = 100 , # We are getting all valid versions to get this inference
351+ advisory_data , # We are getting all valid versions to get this inference
352+ confidence = 100 ,
345353 affected_purls = affected_packages ,
346354 fixed_purl = fixed_package ,
347355 )
0 commit comments