Skip to content
Merged
75 changes: 75 additions & 0 deletions scripts/advisory_publication/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# OCP Advisory Publication Report

## Description

Every week for every OCP release MicroShift team decide if a new MicroShift version should be publish.
The decision is done if there are important changes/fixes to the published.
This script will generate a report with advisory, CVEs and jira ticket relevant info for every OCP version to make the decision.

### Steps
1. Find advisory ids for a OCP version from [ocp-build-data repository](https://github.com/openshift-eng/ocp-build-data)
- For example, for release `4.17` check [`releases.yml`](https://github.com/openshift-eng/ocp-build-data/blob/openshift-4.17/releases.yml)
2. Get list of CVEs from a Red Hat advisory using [Red Hat errata API](https://errata.devel.redhat.com/documentation/developer-guide/api-http-api.html#get-cveshowerrata_id.json)
- Only available behind the Red Hat VPN
- Example for [`144556` advisory](https://errata.devel.redhat.com/cve/show/144556.json)
3. Query Jira to find if there are any MicroShift ticket to address a CVE fix
- Example for [`summary ~ "CVE-2024-21626" AND component = MicroShift and (affectedVersion = 4.17 or affectedVersion = 4.17.z)`](https://issues.redhat.com/issues/?jql=summary%20%20~%20%22CVE-2024-21626%22%20AND%20component%20%3D%20MicroShift%20and%20(affectedVersion%20%3D%204.17%20or%20affectedVersion%20%3D%204.17.z)) query

## Requisites

### Jira API token

Visit [the Profile page on the Jira
Server](https://issues.redhat.com/secure/ViewProfile.jspa?selectedTab=com.atlassian.pats.pats-plugin:jira-user-personal-access-tokens) and create a token.

Set the `JIRA_API_TOKEN` in your env:

```
export JIRA_API_TOKEN="TOKEN_VALUE"
```

### Connect to Red Hat VPN

Red Hat VPN connection is mandatory to get info from Red Hat errata tool.

## Generate report

Run `./scripts/advisory_publication/advisory_publication_report.sh X.Y.Z` to generate json report.

`X.Y.Z` is the target OCP version, for example `4.17.12`

Output format:

```
{
"RHSA-YYYY:NNNNN": {
"type": "extras",
"url": "https://errata.devel.redhat.com/advisory/XXXXX",
"cves": {
"CVE-YYYY-NNNN": {},
"CVE-YYYY-NNNN": {}
}
},
"RHSA-2025:0364": {
"type": "image",
"url": "https://errata.devel.redhat.com/advisory/YYYYY",
"cves": {
"CVE-YYYY-NNNN": {},
"CVE-YYYY-NNNN": {
"jira_ticket": {
"id": "OCPBUGS-MMMMM",
"summary": "CVE-YYYY-NNNN title of the jira ticket",
"status": "Closed",
"resolution": "Not a Bug"
}
}
}
},
"RHSA-YYYY:NNNNN": {
"type": "metadata",
"url": "https://errata.devel.redhat.com/advisory/ZZZZZ",
"cves": {}
}
}

```
137 changes: 137 additions & 0 deletions scripts/advisory_publication/advisory_publication_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env python3

import os
import sys
import jira.client
import requests
import urllib3
import json
import jira
import yaml

SERVER_URL = 'https://issues.redhat.com/'
JIRA_API_TOKEN = os.environ.get('JIRA_API_TOKEN')


def usage():
print("""\
usage: advisory_publication_report.py OCP_VERSION

arguments:
OCP_VERSION: The OCP versions to analyse if MicroShift version should be published. Format: "4.X.Z"\
""")


def get_advisories(ocp_version: str) -> dict[str, int]:
"""
Get a list of advisory ids for a OCP version from github.com/openshift-eng/ocp-build-data repository
Parameters:
ocp_version (str): OCP version with format: "X.Y.Z"
Returns:
(dict): advisory dict with type and id
"""
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

try:
microshift_xy_version = '.'.join(ocp_version.split('.')[:2])
request = requests.get(f'https://raw.githubusercontent.com/openshift-eng/ocp-build-data/refs/heads/openshift-{microshift_xy_version}/releases.yml', verify=False)
request.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
releases_dict = yaml.load(str(request.text), Loader=yaml.SafeLoader)

if ocp_version in releases_dict['releases']:
return releases_dict['releases'][ocp_version]['assembly']['group']['advisories']
else:
raise KeyError(f"{ocp_version} OCP version does NOT exist")


def get_advisory_info(advisory_id: int) -> dict[str, str]:
"""
Get a list of strings with the CVEs ids for an advisory
Parameters:
advisory_id (int): advisory id
Returns:
(list): list of strings with CVE ids
"""
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

try:
request = requests.get(f'https://errata.devel.redhat.com/cve/show/{advisory_id}.json', verify=False)
request.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
advisory_info = json.loads(request.text)

if advisory_info is None:
raise ValueError
if not isinstance(advisory_info, dict):
raise TypeError
return advisory_info


def search_microshift_tickets(affects_version: str, cve_id: str) -> jira.client.ResultList:
"""
Query Jira for MicroShift ticket with CVE id and MicroShift version
Parameters:
affects_version (str): MicroShift affected version with format: "X.Y"
cve_id (str): the CVE id with format: "CVE-YYYY-NNNNN"
Returns:
(jira.client.ResultList): a list with all the Jira tickets matching the query
"""
server = jira.JIRA(server=SERVER_URL, token_auth=JIRA_API_TOKEN)
jira_tickets = server.search_issues(f'''
summary ~ "{cve_id}" and component = MicroShift and (affectedVersion = {affects_version} or affectedVersion = {affects_version}.z)
''')

if not isinstance(jira_tickets, jira.client.ResultList):
raise TypeError
return jira_tickets


def get_report(ocp_version: str) -> dict[str, dict]:
"""
Get a json object with all the advisories, CVEs and jira tickets linked
Parameters:
ocp_version (str): OCP version with format: "X.Y.Z"
Returns:
(dict): json object with all the advisories, CVEs and jira tickets linked
"""
result_json = dict()
advisories = get_advisories(ocp_version)
for advisory_type, advisory_id in advisories.items():
advisory_info = get_advisory_info(advisory_id)
cve_list = advisory_info['cve']
advisory_dict = dict()
advisory_dict['type'] = advisory_type
advisory_dict['url'] = f'https://errata.devel.redhat.com/advisory/{advisory_id}'
advisory_dict['cves'] = dict()
for cve in cve_list:
jira_tickets = search_microshift_tickets(".".join(ocp_version.split(".")[:2]), cve)
advisory_dict['cves'][cve] = dict()
for ticket in jira_tickets:
jira_ticket_dict = dict()
jira_ticket_dict['id'] = ticket.key
jira_ticket_dict['summary'] = ticket.fields.summary
jira_ticket_dict['status'] = ticket.fields.status.name
jira_ticket_dict['resolution'] = ticket.fields.resolution.name
advisory_dict['cves'][cve]['jira_ticket'] = jira_ticket_dict
result_json[advisory_info['advisory']] = advisory_dict
return result_json


def main():
if len(sys.argv) != 2:
usage()
raise ValueError('Invalid number of arguments')

if JIRA_API_TOKEN is None:
raise ValueError('JIRA_API_TOKEN var not found in the env')

ocp_version = str(sys.argv[1])
result_json = get_report(ocp_version)
print(f"{json.dumps(result_json, indent=4)}")


if __name__ == '__main__':
main()
17 changes: 17 additions & 0 deletions scripts/advisory_publication/advisory_publication_report.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash

set -e

SCRIPTDIR="$(dirname "${BASH_SOURCE[0]}")"
REPOROOT="$(git rev-parse --show-toplevel)"
OUTPUT_DIR="${REPOROOT}/_output"
ENVDIR="${OUTPUT_DIR}/advisory_publication"

if [ ! -d "${ENVDIR}" ]; then
echo "Setting up required tools..."
mkdir -p "${OUTPUT_DIR}"
python3 -m venv "${ENVDIR}"
"${ENVDIR}/bin/pip3" install -r "${SCRIPTDIR}/requirements.txt"
fi

"${ENVDIR}/bin/python3" "${SCRIPTDIR}/advisory_publication_report.py" "$@"
3 changes: 3 additions & 0 deletions scripts/advisory_publication/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
requests>=2.32.2
jira>=3.5.0
PyYAML>=6.0.2