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
+ )
42
54
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" )
55
+ response = requests .get (releases_url , verify = False )
56
+ response .raise_for_status ()
47
57
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 } " )
48
62
49
- def get_advisory_info (advisory_id : int ) -> dict [str , str ]:
63
+
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
+
80
+ try :
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
+
111
+ except requests .exceptions .HTTPError as err :
112
+ raise SystemExit (f"GitLab API error while fetching MR changes: { err } " )
113
+
114
+
115
+ def get_advisories (ocp_version : str ) -> dict [str , str ]:
116
+ """
117
+ Get a list of advisory URLs for a OCP version from GitLab merge request YAML files.
118
+
119
+ Parameters:
120
+ ocp_version (str): OCP version with format: "X.Y.Z"
121
+
122
+ Returns:
123
+ dict: advisory dict with type and URL
124
+ """
125
+ # Get MR URL from GitHub releases.yml
126
+ mr_url = get_shipment_merge_request_url (ocp_version )
127
+
128
+ # Convert web URL to API URL
129
+ mr_iid = mr_url .split ('/' )[- 1 ]
130
+ encoded_project_id = quote (GITLAB_PROJECT_ID , safe = '' )
131
+ api_url = f'{ GITLAB_BASE_URL } /api/v4/projects/{ encoded_project_id } /merge_requests/{ mr_iid } '
132
+
133
+ headers = {'PRIVATE-TOKEN' : GITLAB_API_TOKEN }
134
+
59
135
try :
60
- request = requests .get (f'https://errata.devel.redhat.com/cve/show/{ advisory_id } .json' , verify = False )
61
- request .raise_for_status ()
136
+ response = requests .get (api_url , headers = headers , verify = False )
137
+ response .raise_for_status ()
138
+ mr_info = response .json ()
62
139
except requests .exceptions .HTTPError as err :
63
- raise SystemExit (err )
64
- advisory_info = json .loads (request .text )
140
+ raise SystemExit (f"GitLab API error: { err } " )
141
+
142
+ # Get YAML files from the merge request
143
+ yaml_files = get_yaml_files_from_mr (mr_info , headers )
144
+
145
+ # Search through all YAML files to find the advisory information
146
+ advisories_found = {}
147
+
148
+ for file_path , yaml_content in yaml_files .items ():
149
+ # Skip the fbc file as requested
150
+ if 'fbc-openshift' in file_path or not yaml_content :
151
+ continue
152
+
153
+ # Extract advisory URL using dict.get() for safer navigation
154
+ public_url = (yaml_content .get ('shipment' , {})
155
+ .get ('environments' , {})
156
+ .get ('stage' , {})
157
+ .get ('advisory' , {})
158
+ .get ('url' , '' ))
159
+
160
+ if public_url :
161
+ # Determine advisory type from filename and extract CVEs from YAML content
162
+ for advisory_type in ['image' , 'extras' , 'metadata' , 'rpm' ]:
163
+ if advisory_type in file_path :
164
+ # Extract CVEs directly from the YAML file content
165
+ def extract_cves_recursively (data ):
166
+ """Recursively search for CVE patterns in the YAML data"""
167
+ cves_found = []
168
+
169
+ if isinstance (data , dict ):
170
+ for key , value in data .items ():
171
+ # Check if key starts with CVE
172
+ if isinstance (key , str ) and key .startswith ('CVE-' ):
173
+ cves_found .append (key )
174
+ # Check if value is a CVE string
175
+ if isinstance (value , str ) and value .startswith ('CVE-' ):
176
+ cves_found .append (value )
177
+ # Recursively search in nested structures
178
+ cves_found .extend (extract_cves_recursively (value ))
179
+ elif isinstance (data , list ):
180
+ for item in data :
181
+ # Check if item is a CVE string
182
+ if isinstance (item , str ) and item .startswith ('CVE-' ):
183
+ cves_found .append (item )
184
+ # Check if item is a dict with CVE key
185
+ if isinstance (item , dict ) and 'key' in item and isinstance (item ['key' ], str ) and item ['key' ].startswith ('CVE-' ):
186
+ cves_found .append (item ['key' ])
187
+ # Recursively search in nested structures
188
+ cves_found .extend (extract_cves_recursively (item ))
189
+ return cves_found
190
+ # Extract advisory name from public URL
191
+ advisory_name = public_url .split ('/' )[- 1 ] if '/' in public_url else public_url
192
+ # Extract CVEs from the entire YAML content
193
+ cves = extract_cves_recursively (yaml_content )
194
+ advisories_found [advisory_type ] = {
195
+ 'name' : advisory_name ,
196
+ 'cves' : list (set (cves )) # Remove duplicates
197
+ }
198
+ break
199
+
200
+ if not advisories_found :
201
+ raise KeyError (f"{ ocp_version } OCP version advisory data not found in any YAML files from the merge request" )
202
+
203
+ return advisories_found
65
204
66
- if advisory_info is None :
67
- raise ValueError
68
- if not isinstance (advisory_info , dict ):
69
- raise TypeError
70
- return advisory_info
71
205
72
206
73
207
def search_microshift_tickets (affects_version : str , cve_id : str ) -> jira .client .ResultList :
74
208
"""
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
209
+ Query Jira for MicroShift ticket with CVE id and MicroShift version.
210
+
211
+ Parameters:
212
+ affects_version (str): MicroShift affected version with format: "X.Y"
213
+ cve_id (str): the CVE id with format: "CVE-YYYY-NNNNN"
214
+
215
+ Returns:
216
+ jira.client.ResultList: a list with all the Jira tickets matching the query
81
217
"""
82
- server = jira .JIRA (server = SERVER_URL , token_auth = JIRA_API_TOKEN )
218
+ server = jira .JIRA (server = JIRA_URL , token_auth = JIRA_API_TOKEN )
83
219
jira_tickets = server .search_issues (f'''
84
220
summary ~ "{ cve_id } " and component = MicroShift and (affectedVersion = { affects_version } or affectedVersion = { affects_version } .z)
85
221
''' )
@@ -91,46 +227,57 @@ def search_microshift_tickets(affects_version: str, cve_id: str) -> jira.client.
91
227
92
228
def get_report (ocp_version : str ) -> dict [str , dict ]:
93
229
"""
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
230
+ Get a json object with all the advisories, CVEs and jira tickets linked.
231
+
232
+ Parameters:
233
+ ocp_version (str): OCP version with format: "X.Y.Z"
234
+
235
+ Returns:
236
+ dict: json object with all the advisories, CVEs and jira tickets linked
99
237
"""
100
- result_json = dict ()
238
+ result_json = {}
101
239
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 ()
240
+ for advisory_type , advisory_data in advisories .items ():
241
+ advisory_name = advisory_data ['name' ]
242
+ cve_list = advisory_data ['cves' ]
243
+ advisory_dict = {
244
+ 'type' : advisory_type ,
245
+ 'cves' : {}
246
+ }
247
+
109
248
for cve in cve_list :
110
249
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
250
+ advisory_dict ['cves' ][cve ] = {}
251
+ if jira_tickets :
252
+ for ticket in jira_tickets :
253
+ jira_ticket_dict = {
254
+ 'id' : ticket .key ,
255
+ 'summary' : ticket .fields .summary ,
256
+ 'status' : ticket .fields .status .name ,
257
+ 'resolution' : str (ticket .fields .resolution )
258
+ }
259
+ advisory_dict ['cves' ][cve ]['jira_ticket' ] = jira_ticket_dict
260
+ result_json [advisory_name ] = advisory_dict
120
261
return result_json
121
262
122
263
123
264
def main ():
265
+ """Main function to run the advisory publication report."""
124
266
if len (sys .argv ) != 2 :
125
267
usage ()
126
268
raise ValueError ('Invalid number of arguments' )
127
269
128
- if JIRA_API_TOKEN is None :
129
- raise ValueError ('JIRA_API_TOKEN var not found in the env' )
270
+ if JIRA_API_TOKEN is None or GITLAB_API_TOKEN is None :
271
+ missing_tokens = []
272
+ if JIRA_API_TOKEN is None :
273
+ missing_tokens .append ('JIRA_API_TOKEN' )
274
+ if GITLAB_API_TOKEN is None :
275
+ missing_tokens .append ('GITLAB_API_TOKEN' )
276
+ raise ValueError (f"Missing required environment variables: { ', ' .join (missing_tokens )} " )
130
277
131
278
ocp_version = str (sys .argv [1 ])
132
279
result_json = get_report (ocp_version )
133
- print (f" { json .dumps (result_json , indent = 4 )} " )
280
+ print (json .dumps (result_json , indent = 4 ))
134
281
135
282
136
283
if __name__ == '__main__' :
0 commit comments