2020# VulnerableCode is a free software code from nexB Inc. and others.
2121# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222
23+ import logging
24+ import re
25+ from collections import namedtuple
26+ from typing import Dict
27+ from typing import Iterable
28+ from typing import List
29+
2330import requests
2431from packageurl import PackageURL
32+ from univers .version_range import RpmVersionRange
2533
2634from vulnerabilities import severity_systems
27- from vulnerabilities .helpers import nearest_patched_package
35+ from vulnerabilities .helpers import get_item
2836from vulnerabilities .helpers import requests_with_5xx_retry
2937from vulnerabilities .importer import AdvisoryData
38+ from vulnerabilities .importer import AffectedPackage
3039from vulnerabilities .importer import Importer
3140from vulnerabilities .importer import Reference
3241from vulnerabilities .importer import VulnerabilitySeverity
3342
34-
35- class RedhatImporter (Importer ):
36- def __enter__ (self ):
37-
38- self .redhat_cves = fetch ()
39-
40- def updated_advisories (self ):
41- processed_advisories = list (map (to_advisory , self .redhat_cves ))
42- return self .batch_advisories (processed_advisories )
43-
43+ logger = logging .getLogger (__name__ )
4444
4545requests_session = requests_with_5xx_retry (max_retries = 5 , backoff_factor = 1 )
4646
4747
48- def fetch ():
49- """
50- Return a list of CVE data mappings fetched from the RedHat API.
51- See:
52- https://access.redhat.com/documentation/en-us/red_hat_security_data_api/1.0/html/red_hat_security_data_api/index
53- """
54- cves = []
48+ def fetch_list_of_cves () -> Iterable [List [Dict ]]:
5549 page_no = 1
56- url_template = "https://access.redhat.com/hydra/rest/securitydata/cve.json?per_page=10000&page={}" # nopep8
57-
5850 cve_data = None
5951 while True :
60- current_url = url_template . format ( page_no )
52+ current_url = f"https://access.redhat.com/hydra/rest/securitydata/cve.json?per_page=10000&page= { page_no } " # nopep8
6153 try :
62- print (f"Fetching: { current_url } " )
6354 response = requests_session .get (current_url )
6455 if response .status_code != requests .codes .ok :
65- # TODO: log me
66- print (f"Failed to fetch results from { current_url } " )
56+ logger .error (f"Failed to fetch results from { current_url } " )
6757 break
6858 cve_data = response .json ()
6959 except Exception as e :
70- # TODO: log me
71- msg = f"Failed to fetch results from { current_url } :\n { e } "
72- print (msg )
60+ logger .error (f"Failed to fetch results from { current_url } { e } " )
7361 break
74-
7562 if not cve_data :
7663 break
77- cves .extend (cve_data )
7864 page_no += 1
65+ yield cve_data
66+
67+
68+ class RedhatImporter (Importer ):
7969
80- return cves
70+ spdx_license_expression = "TBD"
71+
72+ def advisory_data (self ) -> Iterable [AdvisoryData ]:
73+ for list_of_redhat_cves in fetch_list_of_cves ():
74+ for redhat_cve in list_of_redhat_cves :
75+ yield to_advisory (redhat_cve )
8176
8277
8378def to_advisory (advisory_data ):
84- affected_purls = []
85- if advisory_data .get ("affected_packages" ):
86- for rpm in advisory_data ["affected_packages" ]:
87- purl = rpm_to_purl (rpm )
88- if purl :
89- affected_purls .append (purl )
79+ affected_packages : List [AffectedPackage ] = []
80+ for rpm in advisory_data .get ("affected_packages" ) or []:
81+ purl = rpm_to_purl (rpm )
82+ if purl :
83+ affected_packages .append (
84+ AffectedPackage (
85+ package = PackageURL (
86+ type = purl .type ,
87+ name = purl .name ,
88+ namespace = purl .namespace ,
89+ qualifiers = purl .qualifiers ,
90+ subpath = purl .subpath ,
91+ ),
92+ affected_version_range = RpmVersionRange .from_native (f"= { purl .version } " ),
93+ fixed_version = None ,
94+ )
95+ )
9096
9197 references = []
9298 bugzilla = advisory_data .get ("bugzilla" )
@@ -114,10 +120,13 @@ def to_advisory(advisory_data):
114120 )
115121 )
116122
117- for rh_adv in advisory_data [ "advisories" ]:
123+ for rh_adv in advisory_data . get ( "advisories" ) or [ ]:
118124 # RH provides 3 types of advisories RHSA, RHBA, RHEA. Only RHSA's contain severity score.
119125 # See https://access.redhat.com/articles/2130961 for more details.
120126
127+ if not isinstance (rh_adv , str ):
128+ continue
129+
121130 if "RHSA" in rh_adv .upper ():
122131 rhsa_data = requests_session .get (
123132 f"https://access.redhat.com/hydra/rest/securitydata/cvrf/{ rh_adv } .json"
@@ -126,7 +135,7 @@ def to_advisory(advisory_data):
126135 rhsa_aggregate_severities = []
127136 if rhsa_data .get ("cvrfdoc" ):
128137 # not all RHSA errata have a corresponding CVRF document
129- value = rhsa_data [ "cvrfdoc" ][ "aggregate_severity" ]
138+ value = get_item ( rhsa_data , "cvrfdoc" , "aggregate_severity" )
130139 rhsa_aggregate_severities .append (
131140 VulnerabilitySeverity (
132141 system = severity_systems .REDHAT_AGGREGATE ,
@@ -166,25 +175,89 @@ def to_advisory(advisory_data):
166175
167176 references .append (Reference (severities = redhat_scores , url = advisory_data ["resource_url" ]))
168177 return AdvisoryData (
169- vulnerability_id = advisory_data [ "CVE" ] ,
170- summary = advisory_data [ "bugzilla_description" ] ,
171- affected_packages = nearest_patched_package ( affected_purls , []) ,
178+ aliases = advisory_data . get ( "CVE" ) or "" ,
179+ summary = advisory_data . get ( "bugzilla_description" ) or "" ,
180+ affected_packages = affected_packages ,
172181 references = references ,
173182 )
174183
175184
185+ # This code has been vendored from scancode.
186+ # https://github.com/nexB/scancode-toolkit/blob/16ae20a343c5332114edac34c7b6fcf2fb6bca74/src/packagedcode/rpm.py#L91
187+ class EVR (namedtuple ("EVR" , "epoch version release" )):
188+ """
189+ The RPM Epoch, Version, Release tuple.
190+ """
191+
192+ def __new__ (self , version , release = None , epoch = None ):
193+ """
194+ note: the sort order of the named tuple is the sort order.
195+ But for creation we put the rarely used epoch last with a default to None.
196+ """
197+ if not isinstance (epoch , int ):
198+ if epoch and epoch .strip ():
199+ raise ValueError ("Invalid epoch: must be a number or empty." )
200+ if not version :
201+ raise ValueError ("Version is required: {}" .format (repr (version )))
202+
203+ return super ().__new__ (EVR , epoch , version , release )
204+
205+ def __str__ (self , * args , ** kwargs ):
206+ return self .to_string ()
207+
208+ def to_string (self ):
209+ if self .release :
210+ vr = f"{ self .version } -{ self .release } "
211+ else :
212+ vr = self .version
213+
214+ if self .epoch :
215+ vr = ":" .join ([str (self .epoch ), vr ])
216+ return vr
217+
218+
219+ # This code has been vendored from scancode.
220+ # https://github.com/nexB/scancode-toolkit/blob/16ae20a343c5332114edac34c7b6fcf2fb6bca74/src/packagedcode/nevra.py#L36
221+ def from_name (rpm_string ):
222+ """
223+ Return an (E, N, V, R, A) tuple given a file name, by splitting
224+ [e:]name-version-release.arch into the four possible subcomponents.
225+ Default epoch, version, release and arch to None if not specified.
226+ Accepts RPM names with and without extensions
227+ """
228+ parse_nevra = re .compile ("^" "(.*)" "-" "([^-]*)" "-" "([^-]*)" "\\ ." "([^.]*)" "$" ).match
229+ m = parse_nevra (rpm_string )
230+ if not m :
231+ return None
232+ n , v , r , a = m .groups ()
233+ if ":" not in v :
234+ return None , n , v , r , a
235+ e , v = v .split (":" , 1 )
236+ e = int (e )
237+ return (e , n , v , r , a )
238+
239+
176240def rpm_to_purl (rpm_string ):
177241 # FIXME: there is code in scancode to handle RPM conversion AND this should
178242 # be all be part of the packageurl library
179243
180244 # FIXME: the comment below is not correct, this is the Epoch in the RPM version and not redhat specific
181245 # Red Hat uses `-:0` instead of just `-` to separate
182246 # package name and version
183- components = rpm_string .split ("-0:" )
184- if len (components ) != 2 :
185- return
186247
187- name , version = components
248+ # This code has been vendored from scancode.
249+ # https://github.com/nexB/scancode-toolkit/blob/16ae20a343c5332114edac34c7b6fcf2fb6bca74/src/packagedcode/rpm.py#L310
250+
251+ envra = from_name (rpm_string )
188252
189- if version [0 ].isdigit ():
190- return PackageURL (namespace = "redhat" , name = name , type = "rpm" , version = version )
253+ if not envra :
254+ return None
255+ sepoch , sname , sversion , srel , sarch = envra
256+ src_evr = EVR (sversion , srel , sepoch ).to_string ()
257+ src_qualifiers = {}
258+ if sarch :
259+ src_qualifiers ["arch" ] = sarch
260+
261+ return PackageURL (
262+ type = "rpm" , namespace = "redhat" , name = sname , version = src_evr , qualifiers = src_qualifiers
263+ )
0 commit comments