Skip to content

Commit da86480

Browse files
Merge pull request #5341 from kasturinarra/fix_advisory_robust
USHIFT-5992: Update script to use release data from konflux
2 parents 87457c4 + 894861f commit da86480

File tree

1 file changed

+208
-67
lines changed

1 file changed

+208
-67
lines changed

scripts/advisory_publication/advisory_publication_report.py

Lines changed: 208 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,215 @@
11
#!/usr/bin/env python3
22

3+
import json
34
import os
45
import sys
5-
import jira.client
6+
from urllib.parse import quote
7+
68
import requests
79
import urllib3
8-
import json
9-
import jira
1010
import yaml
1111

12-
SERVER_URL = 'https://issues.redhat.com/'
12+
import jira
13+
import jira.client
14+
15+
JIRA_URL = 'https://issues.redhat.com/'
1316
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'
1420

1521

1622
def usage():
23+
"""Print usage information."""
1724
print("""\
1825
usage: advisory_publication_report.py OCP_VERSION
1926
2027
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\
2233
""")
2334

2435

25-
def get_advisories(ocp_version: str) -> dict[str, int]:
36+
def get_shipment_merge_request_url(ocp_version: str) -> str:
2637
"""
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
3245
"""
3346
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
3447

3548
try:
3649
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()
4257

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}")
4762

4863

49-
def get_advisory_info(advisory_id: int) -> dict[str, str]:
64+
def get_yaml_files_from_mr(mr_info: dict, headers: dict) -> dict:
5065
"""
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
5674
"""
5775
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
5876

77+
encoded_project_id = quote(GITLAB_PROJECT_ID, safe='')
78+
mr_iid = mr_info['iid']
79+
5980
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+
62111
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}")
65113

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
71199

72200

73201
def search_microshift_tickets(affects_version: str, cve_id: str) -> jira.client.ResultList:
74202
"""
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
81211
"""
82-
server = jira.JIRA(server=SERVER_URL, token_auth=JIRA_API_TOKEN)
212+
server = jira.JIRA(server=JIRA_URL, token_auth=JIRA_API_TOKEN)
83213
jira_tickets = server.search_issues(f'''
84214
summary ~ "{cve_id}" and component = MicroShift and (affectedVersion = {affects_version} or affectedVersion = {affects_version}.z)
85215
''')
@@ -91,46 +221,57 @@ def search_microshift_tickets(affects_version: str, cve_id: str) -> jira.client.
91221

92222
def get_report(ocp_version: str) -> dict[str, dict]:
93223
"""
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
99231
"""
100-
result_json = dict()
232+
result_json = {}
101233
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+
109242
for cve in cve_list:
110243
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
120255
return result_json
121256

122257

123258
def main():
259+
"""Main function to run the advisory publication report."""
124260
if len(sys.argv) != 2:
125261
usage()
126262
raise ValueError('Invalid number of arguments')
127263

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)}")
130271

131272
ocp_version = str(sys.argv[1])
132273
result_json = get_report(ocp_version)
133-
print(f"{json.dumps(result_json, indent=4)}")
274+
print(json.dumps(result_json, indent=4))
134275

135276

136277
if __name__ == '__main__':

0 commit comments

Comments
 (0)