blob: 49387ddfd515f6a6893132f8bfee601c2d39a89e [file] [log] [blame]
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +03001#!groovy
2
Ivan Udovichenko314a0732020-04-13 22:47:26 +03003package com.mirantis.mk
4
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +03005import groovy.json.JsonSlurper
Ivan Udovichenko6c337562020-08-06 16:22:26 +03006import groovy.json.JsonOutput
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +03007
8def callREST (String uri, String auth,
Ivan Udovichenko45252252020-04-14 22:45:53 +03009 String method = 'GET', String message = null) {
10 String authEnc = auth.bytes.encodeBase64()
11 def req = new URL(uri).openConnection()
12 req.setRequestMethod(method)
13 req.setRequestProperty('Content-Type', 'application/json')
14 req.setRequestProperty('Authorization', "Basic ${authEnc}")
15 if (message) {
16 req.setDoOutput(true)
17 req.getOutputStream().write(message.getBytes('UTF-8'))
18 }
19 Integer responseCode = req.getResponseCode()
20 String responseText = ''
21 if (responseCode == 200 || responseCode == 201) {
22 responseText = req.getInputStream().getText()
23 }
24 req = null
25 return [ 'responseCode': responseCode, 'responseText': responseText ]
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +030026}
27
28def getTeam (String image = '') {
29 def team_assignee = ''
30 switch(image) {
31 case ~/^(tungsten|tungsten-operator)\/.*$/:
32 team_assignee = 'OpenContrail'
33 break
Ivan Udovichenko8c78c352020-12-14 22:39:47 +030034 case ~/^(bm|general)\/.*$/:
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +030035 team_assignee = 'BM/OS (KaaS BM)'
36 break
37 case ~/^openstack\/.*$/:
38 team_assignee = 'OpenStack hardening'
39 break
40 case ~/^stacklight\/.*$/:
41 team_assignee = 'Stacklight LMA'
42 break
43 case ~/^ceph\/.*$/:
44 team_assignee = 'Storage'
45 break
Ivan Udovichenko5d755da2021-08-09 14:58:56 +030046 case ~/^(core|iam|lcm)\/.*$/:
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +030047 team_assignee = 'KaaS'
48 break
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +030049 default:
50 team_assignee = 'Release Engineering'
51 break
52 }
53
54 return team_assignee
55}
56
57def updateDictionary (String jira_issue_key, Map dict, String uri, String auth, String jira_user_id) {
58 def response = callREST("${uri}/${jira_issue_key}", auth)
59 if ( response['responseCode'] == 200 ) {
60 def issueJSON = new JsonSlurper().parseText(response["responseText"])
61 if (issueJSON.containsKey('fields')) {
62 if (!dict.containsKey(jira_issue_key)) {
63 dict[jira_issue_key] = [
64 summary : '',
65 description: '',
66 comments: []
67 ]
68 }
69 if (issueJSON['fields'].containsKey('summary')){
70 dict[jira_issue_key].summary = issueJSON['fields']['summary']
71 }
72 if (issueJSON['fields'].containsKey('description')) {
73 dict[jira_issue_key].description = issueJSON['fields']['description']
74 }
75 if (issueJSON['fields'].containsKey('comment') && issueJSON['fields']['comment']['comments']) {
76 issueJSON['fields']['comment']['comments'].each {
77 if (it.containsKey('author') && it['author'].containsKey('accountId') && it['author']['accountId'] == jira_user_id) {
78 dict[jira_issue_key]['comments'].add(it['body'])
79 }
80 }
81 }
82 }
83 }
84 return dict
85}
86
87def cacheLookUp(Map dict, String image_short_name, String image_full_name = '', String cve_id = '' ) {
88 def found_key = ['','']
89 if (!found_key[0] && dict && image_short_name) {
90 dict.each { issue_key_name ->
91 if (!found_key[0]) {
Ivan Udovichenkobfe3aca2020-12-17 21:48:50 +030092 def s = dict[issue_key_name.key]['summary'] =~ /(?<=[\/\[])${image_short_name}(?=\])/
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +030093 if (s) {
94 if (image_full_name) {
95 def d = dict[issue_key_name.key]['description'] =~ /(?m)\b${image_full_name}\b/
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +030096 if (d) {
97 found_key = [issue_key_name.key,'']
98 } else {
99 if (dict[issue_key_name.key]['comments']) {
100 def comment_match = false
101 dict[issue_key_name.key]['comments'].each{ comment ->
102 if (!comment_match) {
103 def c = comment =~ /(?m)\b${image_full_name}\b/
104 if (c) {
105 comment_match = true
106 }
107 }
108 }
109 if (!comment_match) {
110 found_key = [issue_key_name.key,'na']
111 } else {
112 found_key = [issue_key_name.key,'']
113 }
114 } else {
115 found_key = [issue_key_name.key,'na']
116 }
117 }
118 }
119 }
120 }
121 }
122 }
123 return found_key
124}
125
Ivan Udovichenko67398552020-12-25 20:13:35 +0300126def getLatestAffectedVersion(cred, productName, defaultJiraAffectedVersion = 'Backlog') {
127 def filterName = ''
128 if (productName == 'mosk') {
129 filterName = 'MOSK'
130 } else if (productName == 'kaas') {
131 filterName = 'KaaS'
132 } else {
133 return defaultJiraAffectedVersion
134 }
135
Ivan Udovichenko4c08b8d2021-01-20 22:15:49 +0000136 def search_api_url = "${cred.description}/rest/api/2/issue/createmeta?projectKeys=PRODX&issuetypeNames=Bug&expand=projects.issuetypes.fields"
Ivan Udovichenko67398552020-12-25 20:13:35 +0300137 def response = callREST("${search_api_url}", "${cred.username}:${cred.password}", 'GET')
138 def InputJSON = new JsonSlurper().parseText(response["responseText"])
139 def AffectedVersions = InputJSON['projects'][0]['issuetypes'][0]['fields']['versions']['allowedValues']
140
141 def versions = []
142 AffectedVersions.each{
Ivan Udovichenko14e87f62021-06-01 17:20:39 +0300143 // 'MOSK' doesn not contain 'released' field
144 if (productName != 'mosk' && it.containsKey('released') && it['released']) {
145 return
146 }
147 if (it.containsKey('name') && it['name'].startsWith(filterName)) {
148 def justVersion = it['name'].replaceAll(/.*_/, '')
149 justVersion = justVersion.replaceAll(/([0-9]+\.)([0-9])$/, '$10$2')
150 versions.add("${justVersion}`${it['name']}")
Ivan Udovichenko67398552020-12-25 20:13:35 +0300151 }
152 }
153 if (versions) {
Ivan Udovichenko14e87f62021-06-01 17:20:39 +0300154 return versions.sort()[0].split('`')[-1]
Ivan Udovichenko67398552020-12-25 20:13:35 +0300155 }
156 return defaultJiraAffectedVersion
157}
158
Ivan Udovichenko31631232021-09-22 13:54:02 +0300159def getNvdInfo(nvdApiUri, cve) {
160 def cveArr = []
161 def response = callREST("${nvdApiUri}/${cve}", '')
162 if (response['responseCode'] == 200) {
163 def InputJSON = new JsonSlurper().parseText(response["responseText"])
164 if (InputJSON.containsKey('impact')) {
165 def cveImpact = InputJSON['impact']
166 ['V3','V2'].each {
167 if (cveImpact.containsKey('baseMetric' + it)) {
168 if (cveImpact['baseMetric' + it].containsKey('cvss' + it)) {
169 if (cveImpact['baseMetric' + it]['cvss' + it].containsKey('baseScore')) {
170 def cveBaseSeverity = ''
171 if (cveImpact['baseMetric' + it]['cvss' + it].containsKey('baseSeverity')) {
172 cveBaseSeverity = cveImpact['baseMetric'+it]['cvss'+it]['baseSeverity']
173 }
174 cveArr.add([it, cveImpact['baseMetric'+it]['cvss'+it]['baseScore'],cveBaseSeverity])
175 }
Ivan Udovichenko16568a62021-09-16 12:50:59 +0300176
Ivan Udovichenko31631232021-09-22 13:54:02 +0300177 }
178 }
179 }
180 }
181 }
182 return cveArr
183}
184
185
186def reportJiraTickets(String reportFileContents, String jiraCredentialsID, String jiraUserID, String productName = '', String ignoreImageListFileContents = '[]', Integer retryTry = 0, String nvdApiUri = '', jiraNamespace = 'PRODX') {
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300187
188 def dict = [:]
189
Ivan Udovichenko6c870b72020-04-14 11:31:51 +0300190 def common = new com.mirantis.mk.Common()
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300191 def cred = common.getCredentialsById(jiraCredentialsID)
192 def auth = "${cred.username}:${cred.password}"
193 def uri = "${cred.description}/rest/api/2/issue"
194
195 def search_api_url = "${cred.description}/rest/api/2/search"
196
Ivan Udovichenko16568a62021-09-16 12:50:59 +0300197
198 def jqlStartAt = 0
199 def jqlStep = 100
200 def jqlProcessedItems = 0
201 def jqlUnfinishedProcess = true
202 def jqlTotalItems = 0
203 while (jqlUnfinishedProcess) {
204 def search_json = """
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300205{
Ivan Udovichenko1c4efd82021-08-17 23:28:30 +0300206 "jql": "reporter = ${jiraUserID} and (labels = cve and labels = security) and (status = 'To Do' or status = 'For Triage' or status = Open or status = 'In Progress' or status = New or status = 'Input Required')", "maxResults":-1
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300207}
208"""
209
Ivan Udovichenko16568a62021-09-16 12:50:59 +0300210 def response = callREST("${search_api_url}", auth, 'POST', search_json)
211 def InputJSON = new JsonSlurper().parseText(response["responseText"])
212 if (InputJSON.containsKey('maxResults')){
213 if (jqlStep > InputJSON['maxResults']) {
214 jqlStep = InputJSON['maxResults']
215 }
216 }
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300217
Ivan Udovichenko16568a62021-09-16 12:50:59 +0300218 jqlStartAt = jqlStartAt + jqlStep
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300219
Ivan Udovichenko16568a62021-09-16 12:50:59 +0300220 if (InputJSON.containsKey('total')){
221 jqlTotalItems = InputJSON['total']
222 }
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300223
Ivan Udovichenko77fd79e2021-09-20 13:15:28 +0300224 if (InputJSON.containsKey('issues')){
Ivan Udovichenko16568a62021-09-16 12:50:59 +0300225 if (!InputJSON['issues'] && retryTry != 0) {
226 throw new Exception('"issues" list is empty')
227 }
228 } else {
229 throw new Exception('Returned JSON from jql does not contain "issues" section')
230 }
231 print 'Temporal debug information:'
232 InputJSON['issues'].each {
233 print it['key'] + ' -> ' + it['fields']['summary']
234 }
235
236 InputJSON['issues'].each {
237 dict[it['key']] = [
238 summary : '',
239 description: '',
240 comments: []
241 ]
242 }
243
244 InputJSON['issues'].each { jira_issue ->
245 dict = updateDictionary(jira_issue['key'], dict, uri, auth, jiraUserID)
246 jqlProcessedItems = jqlProcessedItems + 1
247 }
248 if (jqlProcessedItems >= jqlTotalItems) {
249 jqlUnfinishedProcess = false
250 }
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300251 }
252
Ivan Udovichenko6c870b72020-04-14 11:31:51 +0300253 def reportJSON = new JsonSlurper().parseText(reportFileContents)
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300254 def imageDict = [:]
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300255 reportJSON.each{
256 image ->
257 if ("${image.value}".contains('issues')) { return }
258 image.value.each{
259 pkg ->
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300260 pkg.value.each{
261 cve ->
262 if (cve[2] && (cve[1].contains('High') || cve[1].contains('Critical'))) {
Ivan Udovichenkoc774c6e2020-04-15 01:55:41 +0300263 if (!imageDict.containsKey(image.key)) {
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300264 imageDict.put(image.key, [:])
265 }
266 if (!imageDict[image.key].containsKey(pkg.key)) {
267 imageDict[image.key].put(pkg.key, [])
268 }
Ivan Udovichenko5c60cde2021-08-12 23:52:58 +0300269 imageDict[image.key][pkg.key].add("[${cve[0]}|${cve[4]}] (${cve[2]}) (${cve[3]}) | ${cve[5]}")
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300270 }
271 }
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300272 }
273 }
274
Ivan Udovichenko5e4ae9c2020-12-26 10:56:48 +0300275 def affectedVersion = ''
276 if (jiraNamespace == 'PRODX') {
277 affectedVersion = getLatestAffectedVersion(cred, productName)
278 }
279
Ivan Udovichenko83b7ffc2021-05-07 12:50:16 +0300280 def ignoreImageList = new JsonSlurper().parseText(ignoreImageListFileContents)
281
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300282 def jira_summary = ''
283 def jira_description = ''
Ivan Udovichenko31631232021-09-22 13:54:02 +0300284 def jira_description_nvd_scoring = []
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300285 imageDict.each{
286 image ->
287 def image_key = image.key.replaceAll(/(^[a-z0-9-.]+.mirantis.(net|com)\/|:.*$)/, '')
Ivan Udovichenko83b7ffc2021-05-07 12:50:16 +0300288
289 // Ignore images listed
290 if ((image.key in ignoreImageList) || (image.key.replaceAll(/:.*$/, '') in ignoreImageList)) {
291 print "\n\nIgnoring ${image.key} as it has been found in Docker image ignore list\n"
292 return
293 }
294
Ivan Udovichenko6c337562020-08-06 16:22:26 +0300295 // Below change was produced due to other workflow for UCP Docker images (RE-274)
Ivan Udovichenko9721c312020-10-01 23:50:38 +0300296 if (image_key.startsWith('lcm/docker/ucp')) {
297 return
Ivan Udovichenko6bc5e182020-10-30 02:57:56 +0300298 } else if (image_key.startsWith('mirantis/ucp') || image_key.startsWith('mirantiseng/ucp')) {
Ivan Udovichenko31631232021-09-22 13:54:02 +0300299 jiraNamespace = 'MKE'
Ivan Udovichenkoe0ea4942021-02-18 17:50:20 +0300300 } else if (image_key.startsWith('mirantis/dtr') || image_key.startsWith('mirantiseng/dtr')) {
301 jiraNamespace = 'ENGDTR'
Ivan Udovichenkofe3f11e2020-09-29 17:31:38 +0300302 } else {
303 jiraNamespace = 'PRODX'
304 }
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300305 jira_summary = "[${image_key}] Found CVEs in Docker image"
Ivan Udovichenko62563612020-10-31 03:46:23 +0300306 jira_description = "${image.key}\n"
Ivan Udovichenko31631232021-09-22 13:54:02 +0300307 def filter_mke_severity = false
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300308 image.value.each{
309 pkg ->
Ivan Udovichenko62563612020-10-31 03:46:23 +0300310 jira_description += "__* ${pkg.key}\n"
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300311 pkg.value.each{
312 cve ->
Ivan Udovichenko62563612020-10-31 03:46:23 +0300313 jira_description += "________${cve}\n"
Ivan Udovichenko31631232021-09-22 13:54:02 +0300314 if (nvdApiUri) {
315 jira_description_nvd_scoring = getNvdInfo(nvdApiUri, cve)
316 jira_description_nvd_scoring.each {
317 jira_description += 'CVSS ' + it.join(' ') + '\n'
318 // According to Vikram there will be no fixes for
319 // CVEs with CVSS base score below 7
320 if (jiraNamespace == 'MKE' && it[0] == 'V3' && it[1].toInteger() >= 7) {
321 filter_mke_severity = true
322 }
323 }
324 } else {
325 print 'nvdApiUri var is not specified.'
326 }
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300327 }
328 }
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300329
Ivan Udovichenko31631232021-09-22 13:54:02 +0300330 if (filter_mke_severity) {
331 print "\n\nIgnoring ${image.key} as it does not have CVEs with CVSS base score >7\n"
332 print jira_description
333 return
334 }
335
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300336 def team_assignee = getTeam(image_key)
337
Ivan Udovichenkoadee8b52020-08-06 17:07:28 +0300338 def basicIssueJSON = new JsonSlurper().parseText('{"fields": {}}')
Ivan Udovichenko6c337562020-08-06 16:22:26 +0300339
Ivan Udovichenkoadee8b52020-08-06 17:07:28 +0300340 basicIssueJSON['fields'] = [
Ivan Udovichenkofe3f11e2020-09-29 17:31:38 +0300341 project:[
342 key:"${jiraNamespace}"
343 ],
Ivan Udovichenko6c337562020-08-06 16:22:26 +0300344 summary:"${jira_summary}",
345 description:"${jira_description}",
346 issuetype:[
Ivan Udovichenkofe3f11e2020-09-29 17:31:38 +0300347 name:'Bug'
Ivan Udovichenko6c337562020-08-06 16:22:26 +0300348 ],
349 labels:[
350 'security',
351 'cve'
352 ]
353 ]
354 if (jiraNamespace == 'PRODX') {
Ivan Udovichenkoadee8b52020-08-06 17:07:28 +0300355 basicIssueJSON['fields']['customfield_19000'] = [value:"${team_assignee}"]
Ivan Udovichenko67398552020-12-25 20:13:35 +0300356 basicIssueJSON['fields']['versions'] = [["name": affectedVersion]]
Ivan Udovichenko49955b22021-08-19 23:22:47 +0300357 if (image_key.startsWith('lcm/')) {
358 basicIssueJSON['fields']['components'] = [["name": 'KaaS: LCM']]
359 }
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300360 }
Ivan Udovichenko31631232021-09-22 13:54:02 +0300361
362 if (jiraNamespace == 'MKE') {
363 // Assign issues by default to Vikram bir Singh, as it was asked by him
364 basicIssueJSON['fields']['assignee'] = ['accountId': '5ddd4d67b95b180d17cecc67']
365 }
366
Ivan Udovichenkoadee8b52020-08-06 17:07:28 +0300367 def post_issue_json = JsonOutput.toJson(basicIssueJSON)
Ivan Udovichenko62563612020-10-31 03:46:23 +0300368 def jira_comment = jira_description.replaceAll(/\n/, '\\\\n')
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300369 def post_comment_json = """
370{
Ivan Udovichenko62563612020-10-31 03:46:23 +0300371 "body": "${jira_comment}"
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300372}
373"""
374 def jira_key = cacheLookUp(dict, image_key, image.key)
375 if (jira_key[0] && jira_key[1] == 'na') {
376 def post_comment_response = callREST("${uri}/${jira_key[0]}/comment", auth, 'POST', post_comment_json)
377 if ( post_comment_response['responseCode'] == 201 ) {
378 def issueCommentJSON = new JsonSlurper().parseText(post_comment_response["responseText"])
Ivan Udovichenko67398552020-12-25 20:13:35 +0300379 print "\n\nComment was posted to ${jira_key[0]} ${affectedVersion} for ${image_key} and ${image.key}"
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300380 } else {
381 print "\nComment to ${jira_key[0]} Jira issue was not posted"
382 }
383 } else if (!jira_key[0]) {
384 def post_issue_response = callREST("${uri}/", auth, 'POST', post_issue_json)
385 if (post_issue_response['responseCode'] == 201) {
386 def issueJSON = new JsonSlurper().parseText(post_issue_response["responseText"])
387 dict = updateDictionary(issueJSON['key'], dict, uri, auth, jiraUserID)
Ivan Udovichenko67398552020-12-25 20:13:35 +0300388 print "\n\nJira issue was created ${issueJSON['key']} ${affectedVersion} for ${image_key} and ${image.key}"
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300389 } else {
390 print "\n${image.key} CVE issues were not published\n"
391 }
392 } else {
Ivan Udovichenko99467752020-04-21 02:28:06 +0300393 print "\n\nNothing to process for ${image_key} and ${image.key}"
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300394 }
Ivan Udovichenko894fd8a2020-04-13 17:24:50 +0300395 }
Ivan Udovichenko314a0732020-04-13 22:47:26 +0300396}
Ivan Udovichenko45252252020-04-14 22:45:53 +0300397
398def find_cves_by_severity(String reportJsonContent, String Severity) {
399 def cves = []
400 def reportJSON = new JsonSlurper().parseText(reportJsonContent)
401 reportJSON.each{
402 image ->
403 image.value.each{
404 pkg ->
405 pkg.value.each{
406 cve ->
407 if (cve[2]) {
408 if (cve[1].contains(Severity)) {
409 cves.add("${pkg.key} ${cve[0]} (${cve[2]})")
410 }
411 }
412 }
413 }
414 }
415 return cves
416}