diff --git a/jira_pr_check.py b/jira_pr_check.py new file mode 100755 index 0000000..bc23c74 --- /dev/null +++ b/jira_pr_check.py @@ -0,0 +1,325 @@ +#!/bin/env python3.11 + +import argparse +import os +import subprocess +import sys +from jira import JIRA +from release_config import release_map, jira_field_map + + +def main(): + parser = argparse.ArgumentParser( + description="Connect to JIRA and verify connection", + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + "--jira-url", + required=True, + help="JIRA server URL (e.g., https://ciqinc.atlassian.net)", + ) + parser.add_argument( + "--jira-user", + required=True, + help="JIRA user email", + ) + parser.add_argument( + "--jira-key", + required=True, + help="JIRA API key", + ) + parser.add_argument( + "--kernel-src-tree", + required=True, + help="Path to kernel source tree repository", + ) + parser.add_argument( + "--merge-target", + required=True, + help="Merge target branch", + ) + parser.add_argument( + "--pr-branch", + required=True, + help="PR branch to checkout", + ) + + args = parser.parse_args() + + # Verify kernel source tree path exists + if not os.path.isdir(args.kernel_src_tree): + print(f"ERROR: Kernel source tree path does not exist: {args.kernel_src_tree}") + sys.exit(1) + + # Connect to JIRA + try: + jira = JIRA(server=args.jira_url, basic_auth=(args.jira_user, args.jira_key)) + except Exception as e: + print(f"ERROR: Failed to connect to JIRA: {e}") + sys.exit(1) + + # Checkout the merge target branch first to ensure it exists + try: + subprocess.run( + ["git", "checkout", args.merge_target], + cwd=args.kernel_src_tree, + check=True, + capture_output=True, + text=True + ) + except subprocess.CalledProcessError as e: + print(f"ERROR: Failed to checkout merge target branch {args.merge_target}: {e.stderr}") + sys.exit(1) + + # Checkout the PR branch + try: + subprocess.run( + ["git", "checkout", args.pr_branch], + cwd=args.kernel_src_tree, + check=True, + capture_output=True, + text=True + ) + except subprocess.CalledProcessError as e: + print(f"ERROR: Failed to checkout PR branch {args.pr_branch}: {e.stderr}") + sys.exit(1) + + # Get commits from merge_target to PR branch + try: + result = subprocess.run( + ["git", "log", "--format=%H", f"{args.merge_target}..{args.pr_branch}"], + cwd=args.kernel_src_tree, + check=True, + capture_output=True, + text=True + ) + commit_shas = result.stdout.strip().split('\n') if result.stdout.strip() else [] + + # Parse each commit and extract header + commits_data = [] + for sha in commit_shas: + if not sha: + continue + + # Get full commit message + result = subprocess.run( + ["git", "show", "--format=%B", "--no-patch", sha], + cwd=args.kernel_src_tree, + check=True, + capture_output=True, + text=True + ) + + commit_msg = result.stdout.strip() + lines = commit_msg.split('\n') + + # Extract summary line (first line) + summary = lines[0] if lines else "" + + # Extract header (start after first blank line, end at next blank line) + header_lines = [] + in_header = False + vuln_tickets = [] + commit_cves = [] + + for i, line in enumerate(lines): + if i == 0: # Skip summary line + continue + if not in_header and line.strip() == "": # First blank line, start of header + in_header = True + continue + if in_header and line.strip() == "": # Second blank line, end of header + break + if in_header: + header_lines.append(line) + stripped = line.strip() + + # Check for jira line with VULN + if stripped.lower().startswith('jira ') and 'VULN-' in stripped: + parts = stripped.split() + for part in parts[1:]: # Skip 'jira' keyword + if part.startswith('VULN-'): + vuln_tickets.append(part) + + # Check for CVE line + if stripped.lower().startswith('cve '): + parts = stripped.split() + for part in parts[1:]: # Skip 'cve' keyword + if part.upper().startswith('CVE-'): + commit_cves.append(part.upper()) + + header = '\n'.join(header_lines) + + # Check VULN tickets against merge target + lts_match = None + issues_list = [] + + if vuln_tickets: + for vuln_id in vuln_tickets: + try: + issue = jira.issue(vuln_id) + + # Get LTS product + lts_product_field = issue.get_field("customfield_10381") + if hasattr(lts_product_field, 'value'): + lts_product = lts_product_field.value + else: + lts_product = str(lts_product_field) if lts_product_field else None + + # Get CVEs from JIRA ticket using customfield_10380 + ticket_cve_field = issue.get_field("customfield_10380") + ticket_cves = set() + + if ticket_cve_field: + # Handle different field types (string, list, or object) + if isinstance(ticket_cve_field, str): + # Split by common delimiters and extract CVE IDs + import re + cve_pattern = r'CVE-\d{4}-\d{4,7}' + ticket_cves.update(re.findall(cve_pattern, ticket_cve_field, re.IGNORECASE)) + elif isinstance(ticket_cve_field, list): + for item in ticket_cve_field: + if isinstance(item, str): + import re + cve_pattern = r'CVE-\d{4}-\d{4,7}' + ticket_cves.update(re.findall(cve_pattern, item, re.IGNORECASE)) + else: + # Try to convert to string + import re + cve_pattern = r'CVE-\d{4}-\d{4,7}' + ticket_cves.update(re.findall(cve_pattern, str(ticket_cve_field), re.IGNORECASE)) + + # Normalize to uppercase + ticket_cves = {cve.upper() for cve in ticket_cves} + + # Compare CVEs between commit and JIRA ticket + if commit_cves and ticket_cves: + commit_cves_set = set(commit_cves) + if not commit_cves_set.issubset(ticket_cves): + missing_in_ticket = commit_cves_set - ticket_cves + issues_list.append({ + 'type': 'error', + 'vuln_id': vuln_id, + 'message': f"CVE mismatch - Commit has {', '.join(sorted(missing_in_ticket))} but VULN ticket does not" + }) + if not ticket_cves.issubset(commit_cves_set): + missing_in_commit = ticket_cves - commit_cves_set + issues_list.append({ + 'type': 'warning', + 'vuln_id': vuln_id, + 'message': f"VULN ticket has {', '.join(sorted(missing_in_commit))} but commit does not" + }) + elif commit_cves and not ticket_cves: + issues_list.append({ + 'type': 'warning', + 'vuln_id': vuln_id, + 'message': f"Commit has CVEs {', '.join(sorted(commit_cves))} but VULN ticket has no CVEs" + }) + elif ticket_cves and not commit_cves: + issues_list.append({ + 'type': 'warning', + 'vuln_id': vuln_id, + 'message': f"VULN ticket has CVEs {', '.join(sorted(ticket_cves))} but commit has no CVEs" + }) + + # Check ticket status + status = issue.fields.status.name + if status != "In Progress": + issues_list.append({ + 'type': 'error', + 'vuln_id': vuln_id, + 'message': f"Status is '{status}', expected 'In Progress'" + }) + + # Check if time is logged + time_spent = issue.fields.timespent + if not time_spent or time_spent == 0: + issues_list.append({ + 'type': 'warning', + 'vuln_id': vuln_id, + 'message': 'No time logged - please log time manually' + }) + + # Check if LTS product matches merge target branch + if lts_product and lts_product in release_map: + expected_branch = release_map[lts_product]["src_git_branch"] + if expected_branch == args.merge_target: + lts_match = True + else: + lts_match = False + issues_list.append({ + 'type': 'error', + 'vuln_id': vuln_id, + 'message': f"LTS product '{lts_product}' expects branch '{expected_branch}', but merge target is '{args.merge_target}'" + }) + else: + issues_list.append({ + 'type': 'error', + 'vuln_id': vuln_id, + 'message': f"LTS product '{lts_product}' not found in release_map" + }) + + except Exception as e: + issues_list.append({ + 'type': 'error', + 'vuln_id': vuln_id, + 'message': f"Failed to retrieve ticket: {e}" + }) + + commits_data.append({ + 'sha': sha, + 'summary': summary, + 'header': header, + 'full_message': commit_msg, + 'vuln_tickets': vuln_tickets, + 'lts_match': lts_match, + 'issues': issues_list + }) + + # Print formatted results + print("\n## JIRA PR Check Results\n") + + commits_with_issues = [c for c in commits_data if c['issues']] + has_errors = False + + if commits_with_issues: + print(f"**{len(commits_with_issues)} commit(s) with issues found:**\n") + + for commit in commits_with_issues: + print(f"### Commit `{commit['sha'][:12]}`") + print(f"**Summary:** {commit['summary']}\n") + + # Group issues by type + errors = [i for i in commit['issues'] if i['type'] == 'error'] + warnings = [i for i in commit['issues'] if i['type'] == 'warning'] + + if errors: + has_errors = True + print("**❌ Errors:**") + for issue in errors: + print(f"- **{issue['vuln_id']}**: {issue['message']}") + print() + + if warnings: + print("**⚠️ Warnings:**") + for issue in warnings: + print(f"- **{issue['vuln_id']}**: {issue['message']}") + print() + else: + print("✅ **No issues found!**\n") + + print(f"\n---\n**Summary:** Checked {len(commits_data)} commit(s) total.") + + # Exit with error code if any errors were found + if has_errors: + sys.exit(1) + + return jira, commits_data + + except subprocess.CalledProcessError as e: + print(f"ERROR: Failed to get commits: {e.stderr}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/release_config.py b/release_config.py new file mode 100644 index 0000000..6835978 --- /dev/null +++ b/release_config.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +Product to branch mapping from content_release.sh and JIRA field mappings +""" + +# JIRA custom field mapping +jira_field_map = { + "summary": "summary", + "description": "description", + "customfield_10380": "CVE", + "customfield_10404": "Ingested CVSS Vector", + "customfield_10410": "CVE Ingested Fix", + "customfield_10382": "sRPM", + "customfield_10409": "CVE Ingestion Source", + "customfield_10384": "Ingested CVSS Score", + "customfield_10386": "Ingested Exploit Status", + "customfield_10383": "Ingested CVE Impact", + "customfield_10408": "Package in Priority List", + "customfield_10411": "CVE Priority", + "customfield_10390": "CIQ CVE Impact", + "customfield_10387": "CIQ CVE Score", + "customfield_10405": "CIQ Score Justification", + "customfield_10381": "LTS Product", +} + +# Product to branch mapping +release_map = { + "lts-9.4": { + "src_git_branch": "ciqlts9_4", + "dist_git_branch": "lts94-9", + "mock_config": "rocky-lts94" + }, + "lts-9.2": { + "src_git_branch": "ciqlts9_2", + "dist_git_branch": "lts92-9", + "mock_config": "rocky-lts92" + }, + "lts-8.8": { + "src_git_branch": "ciqlts8_8", + "dist_git_branch": "lts88-8", + "mock_config": "rocky-lts88" + }, + "lts-8.6": { + "src_git_branch": "ciqlts8_6", + "dist_git_branch": "lts86-8", + "mock_config": "rocky-lts86" + }, + "cbr-7.9": { + "src_git_branch": "ciqcbr7_9", + "dist_git_branch": "cbr79-7", + "mock_config": "centos-cbr79" + }, + "fips-9.2": { + "src_git_branch": "fips-9-compliant/5.14.0-284.30.1", + "dist_git_branch": "el92-fips-compliant-9", + "mock_config": "rocky-fips92" + }, + "fips-8.10": { + "src_git_branch": "fips-8-compliant/4.18.0-553.16.1", + "dist_git_branch": "el810-fips-compliant-8", + "mock_config": "rocky-fips810-553-depot" + }, + "fips-8.6": { + "src_git_branch": "fips-8-compliant/4.18.0-553.16.1", + "dist_git_branch": "el86-fips-compliant-8", + "mock_config": "rocky-fips86-553-depot" + }, + "fipslegacy-8.6": { + "src_git_branch": "fips-legacy-8-compliant/4.18.0-425.13.1", + "dist_git_branch": "fips-compliant8", + "mock_config": "rocky-lts86-fips" + } +}