Skip to content

Commit f7311e2

Browse files
committed
[PR CHECKER] JIRA check for PR Headers
This is the first start at a parse and check status of vulns in a PR. It requires a jira url, user, token, target merge branch and the current branch name. It will look for VULNS in the CIQ header and check their LTS product versus the target branch, if they're in the correct status and if they have any time logged.
1 parent 0f5d826 commit f7311e2

File tree

2 files changed

+398
-0
lines changed

2 files changed

+398
-0
lines changed

jira_pr_check.py

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
#!/bin/env python3.11
2+
3+
import argparse
4+
import os
5+
import subprocess
6+
import sys
7+
from jira import JIRA
8+
from release_config import release_map, jira_field_map
9+
10+
11+
def main():
12+
parser = argparse.ArgumentParser(
13+
description="Connect to JIRA and verify connection",
14+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
15+
)
16+
parser.add_argument(
17+
"--jira-url",
18+
required=True,
19+
help="JIRA server URL (e.g., https://ciqinc.atlassian.net)",
20+
)
21+
parser.add_argument(
22+
"--jira-user",
23+
required=True,
24+
help="JIRA user email",
25+
)
26+
parser.add_argument(
27+
"--jira-key",
28+
required=True,
29+
help="JIRA API key",
30+
)
31+
parser.add_argument(
32+
"--kernel-src-tree",
33+
required=True,
34+
help="Path to kernel source tree repository",
35+
)
36+
parser.add_argument(
37+
"--merge-target",
38+
required=True,
39+
help="Merge target branch",
40+
)
41+
parser.add_argument(
42+
"--pr-branch",
43+
required=True,
44+
help="PR branch to checkout",
45+
)
46+
47+
args = parser.parse_args()
48+
49+
# Verify kernel source tree path exists
50+
if not os.path.isdir(args.kernel_src_tree):
51+
print(f"ERROR: Kernel source tree path does not exist: {args.kernel_src_tree}")
52+
sys.exit(1)
53+
54+
# Connect to JIRA
55+
try:
56+
jira = JIRA(server=args.jira_url, basic_auth=(args.jira_user, args.jira_key))
57+
except Exception as e:
58+
print(f"ERROR: Failed to connect to JIRA: {e}")
59+
sys.exit(1)
60+
61+
# Checkout the merge target branch first to ensure it exists
62+
try:
63+
subprocess.run(
64+
["git", "checkout", args.merge_target],
65+
cwd=args.kernel_src_tree,
66+
check=True,
67+
capture_output=True,
68+
text=True
69+
)
70+
except subprocess.CalledProcessError as e:
71+
print(f"ERROR: Failed to checkout merge target branch {args.merge_target}: {e.stderr}")
72+
sys.exit(1)
73+
74+
# Checkout the PR branch
75+
try:
76+
subprocess.run(
77+
["git", "checkout", args.pr_branch],
78+
cwd=args.kernel_src_tree,
79+
check=True,
80+
capture_output=True,
81+
text=True
82+
)
83+
except subprocess.CalledProcessError as e:
84+
print(f"ERROR: Failed to checkout PR branch {args.pr_branch}: {e.stderr}")
85+
sys.exit(1)
86+
87+
# Get commits from merge_target to PR branch
88+
try:
89+
result = subprocess.run(
90+
["git", "log", "--format=%H", f"{args.merge_target}..{args.pr_branch}"],
91+
cwd=args.kernel_src_tree,
92+
check=True,
93+
capture_output=True,
94+
text=True
95+
)
96+
commit_shas = result.stdout.strip().split('\n') if result.stdout.strip() else []
97+
98+
# Parse each commit and extract header
99+
commits_data = []
100+
for sha in commit_shas:
101+
if not sha:
102+
continue
103+
104+
# Get full commit message
105+
result = subprocess.run(
106+
["git", "show", "--format=%B", "--no-patch", sha],
107+
cwd=args.kernel_src_tree,
108+
check=True,
109+
capture_output=True,
110+
text=True
111+
)
112+
113+
commit_msg = result.stdout.strip()
114+
lines = commit_msg.split('\n')
115+
116+
# Extract summary line (first line)
117+
summary = lines[0] if lines else ""
118+
119+
# Extract header (start after first blank line, end at next blank line)
120+
header_lines = []
121+
in_header = False
122+
vuln_tickets = []
123+
commit_cves = []
124+
125+
for i, line in enumerate(lines):
126+
if i == 0: # Skip summary line
127+
continue
128+
if not in_header and line.strip() == "": # First blank line, start of header
129+
in_header = True
130+
continue
131+
if in_header and line.strip() == "": # Second blank line, end of header
132+
break
133+
if in_header:
134+
header_lines.append(line)
135+
stripped = line.strip()
136+
137+
# Check for jira line with VULN
138+
if stripped.lower().startswith('jira ') and 'VULN-' in stripped:
139+
parts = stripped.split()
140+
for part in parts[1:]: # Skip 'jira' keyword
141+
if part.startswith('VULN-'):
142+
vuln_tickets.append(part)
143+
144+
# Check for CVE line
145+
if stripped.lower().startswith('cve '):
146+
parts = stripped.split()
147+
for part in parts[1:]: # Skip 'cve' keyword
148+
if part.upper().startswith('CVE-'):
149+
commit_cves.append(part.upper())
150+
151+
header = '\n'.join(header_lines)
152+
153+
# Check VULN tickets against merge target
154+
lts_match = None
155+
issues_list = []
156+
157+
if vuln_tickets:
158+
for vuln_id in vuln_tickets:
159+
try:
160+
issue = jira.issue(vuln_id)
161+
162+
# Get LTS product
163+
lts_product_field = issue.get_field("customfield_10381")
164+
if hasattr(lts_product_field, 'value'):
165+
lts_product = lts_product_field.value
166+
else:
167+
lts_product = str(lts_product_field) if lts_product_field else None
168+
169+
# Get CVEs from JIRA ticket using customfield_10380
170+
ticket_cve_field = issue.get_field("customfield_10380")
171+
ticket_cves = set()
172+
173+
if ticket_cve_field:
174+
# Handle different field types (string, list, or object)
175+
if isinstance(ticket_cve_field, str):
176+
# Split by common delimiters and extract CVE IDs
177+
import re
178+
cve_pattern = r'CVE-\d{4}-\d{4,7}'
179+
ticket_cves.update(re.findall(cve_pattern, ticket_cve_field, re.IGNORECASE))
180+
elif isinstance(ticket_cve_field, list):
181+
for item in ticket_cve_field:
182+
if isinstance(item, str):
183+
import re
184+
cve_pattern = r'CVE-\d{4}-\d{4,7}'
185+
ticket_cves.update(re.findall(cve_pattern, item, re.IGNORECASE))
186+
else:
187+
# Try to convert to string
188+
import re
189+
cve_pattern = r'CVE-\d{4}-\d{4,7}'
190+
ticket_cves.update(re.findall(cve_pattern, str(ticket_cve_field), re.IGNORECASE))
191+
192+
# Normalize to uppercase
193+
ticket_cves = {cve.upper() for cve in ticket_cves}
194+
195+
# Compare CVEs between commit and JIRA ticket
196+
if commit_cves and ticket_cves:
197+
commit_cves_set = set(commit_cves)
198+
if not commit_cves_set.issubset(ticket_cves):
199+
missing_in_ticket = commit_cves_set - ticket_cves
200+
issues_list.append({
201+
'type': 'error',
202+
'vuln_id': vuln_id,
203+
'message': f"CVE mismatch - Commit has {', '.join(sorted(missing_in_ticket))} but VULN ticket does not"
204+
})
205+
if not ticket_cves.issubset(commit_cves_set):
206+
missing_in_commit = ticket_cves - commit_cves_set
207+
issues_list.append({
208+
'type': 'warning',
209+
'vuln_id': vuln_id,
210+
'message': f"VULN ticket has {', '.join(sorted(missing_in_commit))} but commit does not"
211+
})
212+
elif commit_cves and not ticket_cves:
213+
issues_list.append({
214+
'type': 'warning',
215+
'vuln_id': vuln_id,
216+
'message': f"Commit has CVEs {', '.join(sorted(commit_cves))} but VULN ticket has no CVEs"
217+
})
218+
elif ticket_cves and not commit_cves:
219+
issues_list.append({
220+
'type': 'warning',
221+
'vuln_id': vuln_id,
222+
'message': f"VULN ticket has CVEs {', '.join(sorted(ticket_cves))} but commit has no CVEs"
223+
})
224+
225+
# Check ticket status
226+
status = issue.fields.status.name
227+
if status != "In Progress":
228+
issues_list.append({
229+
'type': 'error',
230+
'vuln_id': vuln_id,
231+
'message': f"Status is '{status}', expected 'In Progress'"
232+
})
233+
234+
# Check if time is logged
235+
time_spent = issue.fields.timespent
236+
if not time_spent or time_spent == 0:
237+
issues_list.append({
238+
'type': 'warning',
239+
'vuln_id': vuln_id,
240+
'message': 'No time logged - please log time manually'
241+
})
242+
243+
# Check if LTS product matches merge target branch
244+
if lts_product and lts_product in release_map:
245+
expected_branch = release_map[lts_product]["src_git_branch"]
246+
if expected_branch == args.merge_target:
247+
lts_match = True
248+
else:
249+
lts_match = False
250+
issues_list.append({
251+
'type': 'error',
252+
'vuln_id': vuln_id,
253+
'message': f"LTS product '{lts_product}' expects branch '{expected_branch}', but merge target is '{args.merge_target}'"
254+
})
255+
else:
256+
issues_list.append({
257+
'type': 'error',
258+
'vuln_id': vuln_id,
259+
'message': f"LTS product '{lts_product}' not found in release_map"
260+
})
261+
262+
except Exception as e:
263+
issues_list.append({
264+
'type': 'error',
265+
'vuln_id': vuln_id,
266+
'message': f"Failed to retrieve ticket: {e}"
267+
})
268+
269+
commits_data.append({
270+
'sha': sha,
271+
'summary': summary,
272+
'header': header,
273+
'full_message': commit_msg,
274+
'vuln_tickets': vuln_tickets,
275+
'lts_match': lts_match,
276+
'issues': issues_list
277+
})
278+
279+
# Print formatted results
280+
print("\n## JIRA PR Check Results\n")
281+
282+
commits_with_issues = [c for c in commits_data if c['issues']]
283+
has_errors = False
284+
285+
if commits_with_issues:
286+
print(f"**{len(commits_with_issues)} commit(s) with issues found:**\n")
287+
288+
for commit in commits_with_issues:
289+
print(f"### Commit `{commit['sha'][:12]}`")
290+
print(f"**Summary:** {commit['summary']}\n")
291+
292+
# Group issues by type
293+
errors = [i for i in commit['issues'] if i['type'] == 'error']
294+
warnings = [i for i in commit['issues'] if i['type'] == 'warning']
295+
296+
if errors:
297+
has_errors = True
298+
print("**❌ Errors:**")
299+
for issue in errors:
300+
print(f"- **{issue['vuln_id']}**: {issue['message']}")
301+
print()
302+
303+
if warnings:
304+
print("**⚠️ Warnings:**")
305+
for issue in warnings:
306+
print(f"- **{issue['vuln_id']}**: {issue['message']}")
307+
print()
308+
else:
309+
print("✅ **No issues found!**\n")
310+
311+
print(f"\n---\n**Summary:** Checked {len(commits_data)} commit(s) total.")
312+
313+
# Exit with error code if any errors were found
314+
if has_errors:
315+
sys.exit(1)
316+
317+
return jira, commits_data
318+
319+
except subprocess.CalledProcessError as e:
320+
print(f"ERROR: Failed to get commits: {e.stderr}")
321+
sys.exit(1)
322+
323+
324+
if __name__ == "__main__":
325+
main()

release_config.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Product to branch mapping from content_release.sh and JIRA field mappings
4+
"""
5+
6+
# JIRA custom field mapping
7+
jira_field_map = {
8+
"summary": "summary",
9+
"description": "description",
10+
"customfield_10380": "CVE",
11+
"customfield_10404": "Ingested CVSS Vector",
12+
"customfield_10410": "CVE Ingested Fix",
13+
"customfield_10382": "sRPM",
14+
"customfield_10409": "CVE Ingestion Source",
15+
"customfield_10384": "Ingested CVSS Score",
16+
"customfield_10386": "Ingested Exploit Status",
17+
"customfield_10383": "Ingested CVE Impact",
18+
"customfield_10408": "Package in Priority List",
19+
"customfield_10411": "CVE Priority",
20+
"customfield_10390": "CIQ CVE Impact",
21+
"customfield_10387": "CIQ CVE Score",
22+
"customfield_10405": "CIQ Score Justification",
23+
"customfield_10381": "LTS Product",
24+
}
25+
26+
# Product to branch mapping
27+
release_map = {
28+
"lts-9.4": {
29+
"src_git_branch": "ciqlts9_4",
30+
"dist_git_branch": "lts94-9",
31+
"mock_config": "rocky-lts94"
32+
},
33+
"lts-9.2": {
34+
"src_git_branch": "ciqlts9_2",
35+
"dist_git_branch": "lts92-9",
36+
"mock_config": "rocky-lts92"
37+
},
38+
"lts-8.8": {
39+
"src_git_branch": "ciqlts8_8",
40+
"dist_git_branch": "lts88-8",
41+
"mock_config": "rocky-lts88"
42+
},
43+
"lts-8.6": {
44+
"src_git_branch": "ciqlts8_6",
45+
"dist_git_branch": "lts86-8",
46+
"mock_config": "rocky-lts86"
47+
},
48+
"cbr-7.9": {
49+
"src_git_branch": "ciqcbr7_9",
50+
"dist_git_branch": "cbr79-7",
51+
"mock_config": "centos-cbr79"
52+
},
53+
"fips-9.2": {
54+
"src_git_branch": "fips-9-compliant/5.14.0-284.30.1",
55+
"dist_git_branch": "el92-fips-compliant-9",
56+
"mock_config": "rocky-fips92"
57+
},
58+
"fips-8.10": {
59+
"src_git_branch": "fips-8-compliant/4.18.0-553.16.1",
60+
"dist_git_branch": "el810-fips-compliant-8",
61+
"mock_config": "rocky-fips810-553-depot"
62+
},
63+
"fips-8.6": {
64+
"src_git_branch": "fips-8-compliant/4.18.0-553.16.1",
65+
"dist_git_branch": "el86-fips-compliant-8",
66+
"mock_config": "rocky-fips86-553-depot"
67+
},
68+
"fipslegacy-8.6": {
69+
"src_git_branch": "fips-legacy-8-compliant/4.18.0-425.13.1",
70+
"dist_git_branch": "fips-compliant8",
71+
"mock_config": "rocky-lts86-fips"
72+
}
73+
}

0 commit comments

Comments
 (0)