Merge "Add artifactory support in  Workflow library"
diff --git a/src/com/mirantis/mk/Atlassian.groovy b/src/com/mirantis/mk/Atlassian.groovy
index 2f067e4..33abced 100644
--- a/src/com/mirantis/mk/Atlassian.groovy
+++ b/src/com/mirantis/mk/Atlassian.groovy
@@ -28,6 +28,8 @@
     String responseText = ''
     if (responseCode == 200) {
         responseText = req.getInputStream().getText()
+    } else if (req.getErrorStream()) {
+        println "Request error: ${req.getErrorStream().getText()}"
     }
     req = null // to reset the connection
     return [ 'responseCode': responseCode, 'responseText': responseText ]
@@ -46,12 +48,18 @@
 **/
 
 List extractJIRA(String commitMsg, String matcherRegex = '([A-Z]+-[0-9]+)') {
-  String msg = new String(commitMsg.decodeBase64())
-  def matcher = (msg =~ matcherRegex)
-  List tickets = []
+    String msg
+    try {
+        msg = new String(commitMsg.decodeBase64())
+    } catch (e) {
+        // use commitMsg as is if cannot decode so we can use the same function for plaintext too
+        msg = commitMsg
+    }
+    def matcher = (msg =~ matcherRegex)
+    List tickets = []
 
-  matcher.each{ tickets.add(it[0]) }
-  return tickets
+    matcher.each{ tickets.add(it[0]) }
+    return tickets
 }
 
 /**
@@ -72,6 +80,23 @@
 
 /**
  *
+ * Update Jira field
+ *
+ * @param uri (string) JIRA url to post message to
+ * @param auth (string) authentication data
+ * @param field (string) name of field to update
+ * @param message (string) json which should update given field. Format depends on field to be updated
+ *
+**/
+
+def updateField(String uri, String auth, String field, String message) {
+    String messageBody = message.replace('"', '\\"').replace('\n', '\\n')
+    String payload = """{"fields": { "${field}": "${messageBody}" }}"""
+    callREST("${uri}", auth, 'PUT', payload)
+}
+
+/**
+ *
  * Post comment to list of JIRA issues.
  *
  * @param uri (string) base JIRA url, each ticket ID appends to it
@@ -84,8 +109,29 @@
 def postMessageToTickets(String uri, String auth, String message, List tickets) {
     tickets.each{
         if ( callREST("${uri}/${it}", auth)['responseCode'] == 200 ) {
-            println "Updating ${uri}/${it} ...".replaceAll('rest/api/2/issue', 'browse')
+            println "Add comment to ${uri}/${it} ...".replaceAll('rest/api/2/issue', 'browse')
             postComment("${uri}/${it}", auth, message)
         }
     }
 }
+
+/**
+ *
+ * Update Jira field on given list of Jira issues
+ *
+ * @param uri (string) base JIRA url, each ticket ID appends to it
+ * @param auth (string) authentication data
+ * @param field (string) name of field to update
+ * @param message (string) json which should update given field. Format depends on field to be updated
+ * @param tickets list of ticket IDs to post message to
+ *
+**/
+
+def updateFieldOnTickets(String uri, String auth, String field, String message, List tickets) {
+    tickets.each{
+        if (callREST("${uri}/${it}", auth)['responseCode'] == 200 ) {
+            println "Update '${field}' field on ${uri}/${it} ...".replaceAll('rest/api/2/issue', 'browse')
+            updateField("${uri}/${it}", auth, field, message)
+        }
+    }
+}
diff --git a/src/com/mirantis/mk/Ceph.groovy b/src/com/mirantis/mk/Ceph.groovy
index 5326321..c959810 100644
--- a/src/com/mirantis/mk/Ceph.groovy
+++ b/src/com/mirantis/mk/Ceph.groovy
@@ -40,8 +40,9 @@
 def removePartition(master, target, partition_uuid, type='', id=-1) {
     def salt = new com.mirantis.mk.Salt()
     def common = new com.mirantis.mk.Common()
-    def partition = ""
+    def partition = ''
     def dev = ''
+    def part_id = ''
     def lvm_enabled = salt.getPillar(master, "I@ceph:osd", "ceph:osd:lvm_enabled")['return'].first().containsValue(true)
     if ( !lvm_enabled ){
         if (type == 'lockbox') {
@@ -55,7 +56,7 @@
         } else if (type == 'data') {
             try {
                 // umount - partition = /dev/sdi2
-                partition = salt.cmdRun(master, target, "df | grep /var/lib/ceph/osd/ceph-${id}")['return'][0].values()[0].split()[0]
+                partition = salt.cmdRun(master, target, "lsblk -rp | grep /var/lib/ceph/osd/ceph-${id}")['return'][0].values()[0].split()[0]
                 salt.cmdRun(master, target, "umount ${partition}")
             } catch (Exception e) {
                 common.warningMsg(e)
@@ -80,14 +81,14 @@
                 // dev = /dev/nvme1n1
                 dev = partition.replaceAll('p\\d+$', "")
                 // part_id = 2
-                def part_id = partition.substring(partition.lastIndexOf("p") + 1).replaceAll("[^0-9]+", "")
+                part_id = partition.substring(partition.lastIndexOf("p") + 1).replaceAll("[^0-9]+", "")
 
             } else {
                 // partition = /dev/sdi2
                 // dev = /dev/sdi
                 dev = partition.replaceAll('\\d+$', "")
                 // part_id = 2
-                def part_id = partition.substring(partition.lastIndexOf("/") + 1).replaceAll("[^0-9]+", "")
+                part_id = partition.substring(partition.lastIndexOf("/") + 1).replaceAll("[^0-9]+", "")
             }
         }
     }
diff --git a/src/com/mirantis/mk/DockerImageScanner.groovy b/src/com/mirantis/mk/DockerImageScanner.groovy
new file mode 100644
index 0000000..14acc3c
--- /dev/null
+++ b/src/com/mirantis/mk/DockerImageScanner.groovy
@@ -0,0 +1,262 @@
+#!groovy
+
+package com.mirantis.mk
+
+import groovy.json.JsonSlurper
+
+def callREST (String uri, String auth,
+			  String method = 'GET', String message = null) {
+	String authEnc = auth.bytes.encodeBase64()
+	def req = new URL(uri).openConnection()
+	req.setRequestMethod(method)
+	req.setRequestProperty('Content-Type', 'application/json')
+	req.setRequestProperty('Authorization', "Basic ${authEnc}")
+	if (message) {
+		req.setDoOutput(true)
+		req.getOutputStream().write(message.getBytes('UTF-8'))
+	}
+	Integer responseCode = req.getResponseCode()
+	String responseText = ''
+	if (responseCode == 200 || responseCode == 201) {
+		responseText = req.getInputStream().getText()
+	}
+	req = null
+	return [ 'responseCode': responseCode, 'responseText': responseText ]
+}
+
+def getTeam (String image = '') {
+    def team_assignee = ''
+    switch(image) {
+        case ~/^(tungsten|tungsten-operator)\/.*$/:
+            team_assignee = 'OpenContrail'
+            break
+        case ~/^bm\/.*$/:
+            team_assignee = 'BM/OS (KaaS BM)'
+            break
+        case ~/^openstack\/.*$/:
+            team_assignee = 'OpenStack hardening'
+            break
+        case ~/^stacklight\/.*$/:
+            team_assignee = 'Stacklight LMA'
+            break
+        case ~/^ceph\/.*$/:
+            team_assignee = 'Storage'
+            break
+        case ~/^iam\/.*$/:
+            team_assignee = 'KaaS'
+            break
+        case ~/^lcm\/.*$/:
+            team_assignee = 'Kubernetes'
+            break
+        default:
+            team_assignee = 'Release Engineering'
+            break
+    }
+
+    return team_assignee
+}
+
+def updateDictionary (String jira_issue_key, Map dict, String uri, String auth, String jira_user_id) {
+    def response = callREST("${uri}/${jira_issue_key}", auth)
+    if ( response['responseCode'] == 200 ) {
+        def issueJSON = new JsonSlurper().parseText(response["responseText"])
+        if (issueJSON.containsKey('fields')) {
+            if (!dict.containsKey(jira_issue_key)) {
+                dict[jira_issue_key] = [
+                        summary : '',
+                        description: '',
+                        comments: []
+                ]
+            }
+            if (issueJSON['fields'].containsKey('summary')){
+                dict[jira_issue_key].summary = issueJSON['fields']['summary']
+            }
+            if (issueJSON['fields'].containsKey('description')) {
+                dict[jira_issue_key].description = issueJSON['fields']['description']
+            }
+            if (issueJSON['fields'].containsKey('comment') && issueJSON['fields']['comment']['comments']) {
+                issueJSON['fields']['comment']['comments'].each {
+                    if (it.containsKey('author') && it['author'].containsKey('accountId') && it['author']['accountId'] == jira_user_id) {
+                        dict[jira_issue_key]['comments'].add(it['body'])
+                    }
+                }
+            }
+        }
+    }
+    return dict
+}
+
+def cacheLookUp(Map dict, String image_short_name, String image_full_name = '', String cve_id = '' ) {
+    def found_key = ['','']
+    if (!found_key[0] && dict && image_short_name) {
+        dict.each { issue_key_name ->
+            if (!found_key[0]) {
+                def s = dict[issue_key_name.key]['summary'] =~ /\b${image_short_name}\b/
+                if (s) {
+                    if (image_full_name) {
+                        def d = dict[issue_key_name.key]['description'] =~ /(?m)\b${image_full_name}\b/
+                        if (d) {
+                            found_key = [issue_key_name.key,'']
+                        } else {
+                            if (dict[issue_key_name.key]['comments']) {
+                                def comment_match = false
+                                dict[issue_key_name.key]['comments'].each{ comment ->
+                                    if (!comment_match) {
+                                        def c = comment =~ /(?m)\b${image_full_name}\b/
+                                        if (c) {
+                                            comment_match = true
+                                        }
+                                    }
+                                }
+                                if (!comment_match) {
+                                    found_key = [issue_key_name.key,'na']
+                                } else {
+                                    found_key = [issue_key_name.key,'']
+                                }
+                            } else {
+                                found_key = [issue_key_name.key,'na']
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return found_key
+}
+
+def reportJiraTickets(String reportFileContents, String jiraCredentialsID, String jiraUserID) {
+
+    def dict = [:]
+
+    def common = new com.mirantis.mk.Common()
+    def cred = common.getCredentialsById(jiraCredentialsID)
+    def auth = "${cred.username}:${cred.password}"
+    def uri = "${cred.description}/rest/api/2/issue"
+
+    def search_api_url = "${cred.description}/rest/api/2/search"
+
+    def search_json = """
+{
+    "jql": "reporter = ${jiraUserID} and (labels = cve and labels = security) and (status = 'To Do' or status = 'For Triage' or status = Open or status = 'In Progress')"
+}
+"""
+
+    def response = callREST("${search_api_url}", auth, 'POST', search_json)
+
+    def InputJSON = new JsonSlurper().parseText(response["responseText"])
+
+    InputJSON['issues'].each {
+        dict[it['key']] = [
+                summary : '',
+                description: '',
+                comments: []
+        ]
+    }
+
+    InputJSON['issues'].each { jira_issue ->
+        dict = updateDictionary(jira_issue['key'], dict, uri, auth, jiraUserID)
+    }
+
+    def reportJSON = new JsonSlurper().parseText(reportFileContents)
+    def imageDict = [:]
+    def cves = []
+    reportJSON.each{
+        image ->
+            if ("${image.value}".contains('issues')) { return }
+            image.value.each{
+                pkg ->
+                    cves = []
+                    pkg.value.each{
+                        cve ->
+                            if (cve[2] && (cve[1].contains('High') || cve[1].contains('Critical'))) {
+                                if (!imageDict.containsKey("${image.key}")) {
+                                    imageDict.put(image.key, [:])
+                                }
+                                if (!imageDict[image.key].containsKey(pkg.key)) {
+                                    imageDict[image.key].put(pkg.key, [])
+                                }
+                                cves.add("${cve[0]} (${cve[2]})")
+                            }
+                    }
+                    if (cves) {
+                        imageDict[image.key] = [
+                                "${pkg.key}": cves
+                        ]
+                    }
+            }
+    }
+
+    def jira_summary = ''
+    def jira_description = ''
+    imageDict.each{
+        image ->
+            def image_key = image.key.replaceAll(/(^[a-z0-9-.]+.mirantis.(net|com)\/|:.*$)/, '')
+            // Temporary exclude tungsten images
+            if (image_key.startsWith('tungsten/') || image_key.startsWith('tungsten-operator/')) { return }
+            jira_summary = "[${image_key}] Found CVEs in Docker image"
+            jira_description = "{noformat}${image.key}\\n"
+            image.value.each{
+                pkg ->
+                    jira_description += "  * ${pkg.key}\\n"
+                    pkg.value.each{
+                        cve ->
+                            jira_description += "      - ${cve}\\n"
+                    }
+            }
+            jira_description += "{noformat}"
+
+            def team_assignee = getTeam(image_key)
+
+            def post_issue_json = """
+{
+    "fields": {
+        "project": {
+            "key": "PRODX"
+        },
+        "summary": "${jira_summary}",
+        "description": "${jira_description}",
+        "issuetype": {
+            "name": "Bug"
+        },
+        "labels": [
+            "security",
+            "cve"
+        ],
+        "customfield_19000": {
+            "value": "${team_assignee}"
+        },
+        "versions": [
+            {
+                "name": "Backlog"
+            }
+        ]
+    }
+}
+"""
+            def post_comment_json = """
+{
+    "body": "${jira_description}"
+}
+"""
+            def jira_key = cacheLookUp(dict, image_key, image.key)
+            if (jira_key[0] && jira_key[1] == 'na') {
+                def post_comment_response = callREST("${uri}/${jira_key[0]}/comment", auth, 'POST', post_comment_json)
+                if ( post_comment_response['responseCode'] == 201 ) {
+                    def issueCommentJSON = new JsonSlurper().parseText(post_comment_response["responseText"])
+                } else {
+                    print "\nComment to ${jira_key[0]} Jira issue was not posted"
+                }
+            } else if (!jira_key[0]) {
+                def post_issue_response = callREST("${uri}/", auth, 'POST', post_issue_json)
+                if (post_issue_response['responseCode'] == 201) {
+                    def issueJSON = new JsonSlurper().parseText(post_issue_response["responseText"])
+                    dict = updateDictionary(issueJSON['key'], dict, uri, auth, jiraUserID)
+                } else {
+                    print "\n${image.key} CVE issues were not published\n"
+                }
+            } else {
+                print "\n\nNothing to process for for ${image_key} and ${image.key}"
+            }
+    }
+}
diff --git a/src/com/mirantis/mk/ReleaseWorkflow.groovy b/src/com/mirantis/mk/ReleaseWorkflow.groovy
index 8cb0f51..1302bbe 100644
--- a/src/com/mirantis/mk/ReleaseWorkflow.groovy
+++ b/src/com/mirantis/mk/ReleaseWorkflow.groovy
@@ -152,6 +152,14 @@
             }
         }
 
+        String status = sh(script: "git -C ${repoDir} status -s", returnStdout: true).trim()
+        if (!status){
+            common.warningMsg('All values seem up to date, nothing to update')
+            return
+        }
+        common.infoMsg("""Next files will be updated:
+                       ${status}
+                       """)
         commitMessage =
                 """${comment}
 
@@ -169,7 +177,6 @@
             }
         }
 
-
         //commit change
         git.commitGitChanges(repoDir, commitMessage, changeAuthorEmail, changeAuthorName, false)
         //post change