1
1
#!/usr/bin/env python3
2
2
3
+ import json
3
4
import os
4
5
import sys
5
- import jira .client
6
+ from urllib .parse import quote
7
+
6
8
import requests
7
9
import urllib3
8
- import json
9
- import jira
10
10
import yaml
11
11
12
- SERVER_URL = 'https://issues.redhat.com/'
12
+ import jira
13
+ import jira .client
14
+
15
+ JIRA_URL = 'https://issues.redhat.com/'
13
16
JIRA_API_TOKEN = os .environ .get ('JIRA_API_TOKEN' )
17
+ GITLAB_API_TOKEN = os .environ .get ('GITLAB_API_TOKEN' )
18
+ GITLAB_BASE_URL = 'https://gitlab.cee.redhat.com'
19
+ GITLAB_PROJECT_ID = 'hybrid-platforms/art/ocp-shipment-data'
14
20
15
21
16
22
def usage ():
23
+ """Print usage information."""
17
24
print ("""\
18
25
usage: advisory_publication_report.py OCP_VERSION
19
26
20
27
arguments:
21
- OCP_VERSION: The OCP versions to analyse if MicroShift version should be published. Format: "4.X.Z"\
28
+ OCP_VERSION: The OCP versions to analyse if MicroShift version should be published. Format: "4.X.Z"
29
+
30
+ environment variables:
31
+ JIRA_API_TOKEN: API token for Jira access
32
+ GITLAB_API_TOKEN: API token for GitLab access\
22
33
""" )
23
34
24
35
25
- def get_advisories (ocp_version : str ) -> dict [ str , int ] :
36
+ def get_shipment_merge_request_url (ocp_version : str ) -> str :
26
37
"""
27
- Get a list of advisory ids for a OCP version from github.com/openshift-eng/ocp-build-data repository
28
- Parameters:
29
- ocp_version (str): OCP version with format: "X.Y.Z"
30
- Returns:
31
- (dict): advisory dict with type and id
38
+ Get merge request URL from GitHub releases.yml file.
39
+
40
+ Parameters:
41
+ ocp_version (str): OCP version with format: "X.Y.Z"
42
+
43
+ Returns:
44
+ str: GitLab merge request URL
32
45
"""
33
46
urllib3 .disable_warnings (urllib3 .exceptions .InsecureRequestWarning )
34
47
35
48
try :
36
49
microshift_xy_version = '.' .join (ocp_version .split ('.' )[:2 ])
37
- request = requests .get (f'https://raw.githubusercontent.com/openshift-eng/ocp-build-data/refs/heads/openshift-{ microshift_xy_version } /releases.yml' , verify = False )
38
- request .raise_for_status ()
39
- except requests .exceptions .HTTPError as err :
40
- raise SystemExit (err )
41
- releases_dict = yaml .load (str (request .text ), Loader = yaml .SafeLoader )
50
+ releases_url = (
51
+ f'https://raw.githubusercontent.com/openshift-eng/ocp-build-data/'
52
+ f'refs/heads/openshift-{ microshift_xy_version } /releases.yml'
53
+ )
54
+
55
+ response = requests .get (releases_url , verify = False )
56
+ response .raise_for_status ()
42
57
43
- if ocp_version in releases_dict [ 'releases' ]:
44
- return releases_dict ['releases' ][ocp_version ]['assembly' ]['group' ]['advisories ' ]
45
- else :
46
- raise KeyError (f"{ ocp_version } OCP version does NOT exist " )
58
+ releases_dict = yaml . load ( response . text , Loader = yaml . SafeLoader )
59
+ return releases_dict ['releases' ][ocp_version ]['assembly' ]['group' ]['shipment' ][ 'url ' ]
60
+ except requests . exceptions . HTTPError as err :
61
+ raise SystemExit (f"Failed to fetch releases.yml: { err } " )
47
62
48
63
49
- def get_advisory_info ( advisory_id : int ) -> dict [ str , str ] :
64
+ def get_yaml_files_from_mr ( mr_info : dict , headers : dict ) -> dict :
50
65
"""
51
- Get a list of strings with the CVEs ids for an advisory
52
- Parameters:
53
- advisory_id (int): advisory id
54
- Returns:
55
- (list): list of strings with CVE ids
66
+ Get YAML files from a merge request.
67
+
68
+ Parameters:
69
+ mr_info (dict): merge request information
70
+ headers (dict): GitLab API headers
71
+
72
+ Returns:
73
+ dict: dictionary containing parsed YAML content
56
74
"""
57
75
urllib3 .disable_warnings (urllib3 .exceptions .InsecureRequestWarning )
58
76
77
+ encoded_project_id = quote (GITLAB_PROJECT_ID , safe = '' )
78
+ mr_iid = mr_info ['iid' ]
79
+
59
80
try :
60
- request = requests .get (f'https://errata.devel.redhat.com/cve/show/{ advisory_id } .json' , verify = False )
61
- request .raise_for_status ()
81
+ # Get changes in the merge request
82
+ url = f'{ GITLAB_BASE_URL } /api/v4/projects/{ encoded_project_id } /merge_requests/{ mr_iid } /changes'
83
+ response = requests .get (url , headers = headers , verify = False )
84
+ response .raise_for_status ()
85
+
86
+ changes = response .json ()
87
+ yaml_content = {}
88
+
89
+ # Process each changed file
90
+ for change in changes .get ('changes' , []):
91
+ file_path = change .get ('new_path' , change .get ('old_path' , 'unknown' ))
92
+
93
+ # Only process YAML files
94
+ if file_path .endswith (('.yml' , '.yaml' )):
95
+ file_url = f'{ GITLAB_BASE_URL } /api/v4/projects/{ encoded_project_id } /repository/files/{ quote (file_path , safe = "" )} /raw'
96
+
97
+ # Try target branch first, then source branch
98
+ file_response = requests .get (file_url , headers = headers , params = {'ref' : mr_info ['target_branch' ]}, verify = False )
99
+
100
+ if file_response .status_code != 200 :
101
+ file_response = requests .get (file_url , headers = headers , params = {'ref' : mr_info ['source_branch' ]}, verify = False )
102
+
103
+ if file_response .status_code == 200 :
104
+ try :
105
+ yaml_content [file_path ] = yaml .load (file_response .text , Loader = yaml .SafeLoader )
106
+ except yaml .YAMLError as e :
107
+ print (f"Warning: Could not parse YAML file { file_path } : { e } " )
108
+
109
+ return yaml_content
110
+
62
111
except requests .exceptions .HTTPError as err :
63
- raise SystemExit (err )
64
- advisory_info = json .loads (request .text )
112
+ raise SystemExit (f"GitLab API error while fetching MR changes: { err } " )
65
113
66
- if advisory_info is None :
67
- raise ValueError
68
- if not isinstance (advisory_info , dict ):
69
- raise TypeError
70
- return advisory_info
114
+
115
+ def extract_cves_recursively (data ):
116
+ """Recursively search for CVE patterns in the YAML data"""
117
+ cves_found = []
118
+
119
+ if isinstance (data , dict ):
120
+ for key , value in data .items ():
121
+ if isinstance (key , str ) and key .startswith ('CVE-' ):
122
+ cves_found .append (key )
123
+ if isinstance (value , str ) and value .startswith ('CVE-' ):
124
+ cves_found .append (value )
125
+ cves_found .extend (extract_cves_recursively (value ))
126
+ elif isinstance (data , list ):
127
+ for item in data :
128
+ if isinstance (item , str ) and item .startswith ('CVE-' ):
129
+ cves_found .append (item )
130
+ if isinstance (item , dict ) and 'key' in item and isinstance (item ['key' ], str ) and item ['key' ].startswith ('CVE-' ):
131
+ cves_found .append (item ['key' ])
132
+ cves_found .extend (extract_cves_recursively (item ))
133
+ return cves_found
134
+
135
+
136
+ def get_advisories (ocp_version : str ) -> dict [str , str ]:
137
+ """
138
+ Get a list of advisory URLs for a OCP version from GitLab merge request YAML files.
139
+
140
+ Parameters:
141
+ ocp_version (str): OCP version with format: "X.Y.Z"
142
+
143
+ Returns:
144
+ dict: advisory dict with type and URL
145
+ """
146
+ # Get MR URL from GitHub releases.yml
147
+ mr_url = get_shipment_merge_request_url (ocp_version )
148
+
149
+ # Convert web URL to API URL
150
+ mr_iid = mr_url .split ('/' )[- 1 ]
151
+ encoded_project_id = quote (GITLAB_PROJECT_ID , safe = '' )
152
+ api_url = f'{ GITLAB_BASE_URL } /api/v4/projects/{ encoded_project_id } /merge_requests/{ mr_iid } '
153
+
154
+ headers = {'PRIVATE-TOKEN' : GITLAB_API_TOKEN }
155
+
156
+ try :
157
+ response = requests .get (api_url , headers = headers , verify = False )
158
+ response .raise_for_status ()
159
+ mr_info = response .json ()
160
+ except requests .exceptions .HTTPError as err :
161
+ raise SystemExit (f"GitLab API error: { err } " )
162
+
163
+ # Get YAML files from the merge request
164
+ yaml_files = get_yaml_files_from_mr (mr_info , headers )
165
+
166
+ # Search through all YAML files to find the advisory information
167
+ advisories_found = {}
168
+
169
+ for file_path , yaml_content in yaml_files .items ():
170
+ # Skip the fbc file as requested
171
+ if 'fbc-openshift' in file_path or not yaml_content :
172
+ continue
173
+
174
+ # Extract advisory URL using dict.get() for safer navigation
175
+ public_url = (yaml_content .get ('shipment' , {})
176
+ .get ('environments' , {})
177
+ .get ('stage' , {})
178
+ .get ('advisory' , {})
179
+ .get ('url' , '' ))
180
+
181
+ if public_url :
182
+ # Determine advisory type from filename and extract CVEs from YAML content
183
+ for advisory_type in ['image' , 'extras' , 'metadata' , 'rpm' ]:
184
+ if advisory_type in file_path :
185
+ # Extract advisory name from public URL
186
+ advisory_name = public_url .split ('/' )[- 1 ] if '/' in public_url else public_url
187
+ # Extract CVEs from the entire YAML content
188
+ cves = extract_cves_recursively (yaml_content )
189
+ advisories_found [advisory_type ] = {
190
+ 'name' : advisory_name ,
191
+ 'cves' : list (set (cves )) # Remove duplicates
192
+ }
193
+ break
194
+
195
+ if not advisories_found :
196
+ raise KeyError (f"{ ocp_version } OCP version advisory data not found in any YAML files from the merge request" )
197
+
198
+ return advisories_found
71
199
72
200
73
201
def search_microshift_tickets (affects_version : str , cve_id : str ) -> jira .client .ResultList :
74
202
"""
75
- Query Jira for MicroShift ticket with CVE id and MicroShift version
76
- Parameters:
77
- affects_version (str): MicroShift affected version with format: "X.Y"
78
- cve_id (str): the CVE id with format: "CVE-YYYY-NNNNN"
79
- Returns:
80
- (jira.client.ResultList): a list with all the Jira tickets matching the query
203
+ Query Jira for MicroShift ticket with CVE id and MicroShift version.
204
+
205
+ Parameters:
206
+ affects_version (str): MicroShift affected version with format: "X.Y"
207
+ cve_id (str): the CVE id with format: "CVE-YYYY-NNNNN"
208
+
209
+ Returns:
210
+ jira.client.ResultList: a list with all the Jira tickets matching the query
81
211
"""
82
- server = jira .JIRA (server = SERVER_URL , token_auth = JIRA_API_TOKEN )
212
+ server = jira .JIRA (server = JIRA_URL , token_auth = JIRA_API_TOKEN )
83
213
jira_tickets = server .search_issues (f'''
84
214
summary ~ "{ cve_id } " and component = MicroShift and (affectedVersion = { affects_version } or affectedVersion = { affects_version } .z)
85
215
''' )
@@ -91,46 +221,57 @@ def search_microshift_tickets(affects_version: str, cve_id: str) -> jira.client.
91
221
92
222
def get_report (ocp_version : str ) -> dict [str , dict ]:
93
223
"""
94
- Get a json object with all the advisories, CVEs and jira tickets linked
95
- Parameters:
96
- ocp_version (str): OCP version with format: "X.Y.Z"
97
- Returns:
98
- (dict): json object with all the advisories, CVEs and jira tickets linked
224
+ Get a json object with all the advisories, CVEs and jira tickets linked.
225
+
226
+ Parameters:
227
+ ocp_version (str): OCP version with format: "X.Y.Z"
228
+
229
+ Returns:
230
+ dict: json object with all the advisories, CVEs and jira tickets linked
99
231
"""
100
- result_json = dict ()
232
+ result_json = {}
101
233
advisories = get_advisories (ocp_version )
102
- for advisory_type , advisory_id in advisories .items ():
103
- advisory_info = get_advisory_info (advisory_id )
104
- cve_list = advisory_info ['cve' ]
105
- advisory_dict = dict ()
106
- advisory_dict ['type' ] = advisory_type
107
- advisory_dict ['url' ] = f'https://errata.devel.redhat.com/advisory/{ advisory_id } '
108
- advisory_dict ['cves' ] = dict ()
234
+ for advisory_type , advisory_data in advisories .items ():
235
+ advisory_name = advisory_data ['name' ]
236
+ cve_list = advisory_data ['cves' ]
237
+ advisory_dict = {
238
+ 'type' : advisory_type ,
239
+ 'cves' : {}
240
+ }
241
+
109
242
for cve in cve_list :
110
243
jira_tickets = search_microshift_tickets ("." .join (ocp_version .split ("." )[:2 ]), cve )
111
- advisory_dict ['cves' ][cve ] = dict ()
112
- for ticket in jira_tickets :
113
- jira_ticket_dict = dict ()
114
- jira_ticket_dict ['id' ] = ticket .key
115
- jira_ticket_dict ['summary' ] = ticket .fields .summary
116
- jira_ticket_dict ['status' ] = ticket .fields .status .name
117
- jira_ticket_dict ['resolution' ] = str (ticket .fields .resolution )
118
- advisory_dict ['cves' ][cve ]['jira_ticket' ] = jira_ticket_dict
119
- result_json [advisory_info ['advisory' ]] = advisory_dict
244
+ advisory_dict ['cves' ][cve ] = {}
245
+ if jira_tickets :
246
+ for ticket in jira_tickets :
247
+ jira_ticket_dict = {
248
+ 'id' : ticket .key ,
249
+ 'summary' : ticket .fields .summary ,
250
+ 'status' : ticket .fields .status .name ,
251
+ 'resolution' : str (ticket .fields .resolution )
252
+ }
253
+ advisory_dict ['cves' ][cve ]['jira_ticket' ] = jira_ticket_dict
254
+ result_json [advisory_name ] = advisory_dict
120
255
return result_json
121
256
122
257
123
258
def main ():
259
+ """Main function to run the advisory publication report."""
124
260
if len (sys .argv ) != 2 :
125
261
usage ()
126
262
raise ValueError ('Invalid number of arguments' )
127
263
128
- if JIRA_API_TOKEN is None :
129
- raise ValueError ('JIRA_API_TOKEN var not found in the env' )
264
+ if JIRA_API_TOKEN is None or GITLAB_API_TOKEN is None :
265
+ missing_tokens = []
266
+ if JIRA_API_TOKEN is None :
267
+ missing_tokens .append ('JIRA_API_TOKEN' )
268
+ if GITLAB_API_TOKEN is None :
269
+ missing_tokens .append ('GITLAB_API_TOKEN' )
270
+ raise ValueError (f"Missing required environment variables: { ', ' .join (missing_tokens )} " )
130
271
131
272
ocp_version = str (sys .argv [1 ])
132
273
result_json = get_report (ocp_version )
133
- print (f" { json .dumps (result_json , indent = 4 )} " )
274
+ print (json .dumps (result_json , indent = 4 ))
134
275
135
276
136
277
if __name__ == '__main__' :
0 commit comments