Vasyl Saienko | 4e8ec64 | 2018-09-17 10:08:08 +0000 | [diff] [blame] | 1 | /** |
| 2 | * |
| 3 | * Run openscap xccdf evaluation on given nodes |
| 4 | * |
| 5 | * Expected parametes: |
| 6 | * SALT_MASTER_URL Full Salt API address. |
| 7 | * SALT_MASTER_CREDENTIALS Credentials to the Salt API. |
| 8 | * |
| 9 | * XCCDF_BENCHMARKS_DIR The XCCDF benchmarks base directory (default /usr/share/xccdf-benchmarks/mirantis/) |
| 10 | * XCCDF_BENCHMARKS List of pairs XCCDF benchmark filename and corresponding profile separated with ',' |
| 11 | * these pairs are separated with semicolon. |
| 12 | * (e.g. manila/openstack_manila-xccdf.xml,profilename;horizon/openstack_horizon-xccdf.xml,profile) |
| 13 | * XCCDF_VERSION The XCCDF version (default 1.2) |
| 14 | * XCCDF_TAILORING_ID The tailoring id (default None) |
| 15 | * |
| 16 | * TARGET_SERVERS The target Salt nodes (default *) |
| 17 | * |
| 18 | * ARTIFACTORY_URL The artifactory URL |
| 19 | * ARTIFACTORY_NAMESPACE The artifactory namespace (default 'mirantis/openscap') |
| 20 | * ARTIFACTORY_REPO The artifactory repo (default 'binary-dev-local') |
| 21 | * |
| 22 | * UPLOAD_TO_DASHBOARD Boolean. Upload results to the WORP or not |
| 23 | * DASHBOARD_API_URL The WORP api base url. Mandatory if UPLOAD_TO_DASHBOARD is true |
| 24 | */ |
| 25 | |
| 26 | |
| 27 | |
| 28 | /** |
| 29 | * Upload results to the `WORP` dashboard |
| 30 | * |
| 31 | * @param apiUrl The base dashboard api url |
| 32 | * @param cloudName The cloud name (mostly, the given node's domain name) |
| 33 | * @param nodeName The node name |
| 34 | * @param results The scanning results |
| 35 | */ |
| 36 | def uploadResultToDashboard(apiUrl, cloudName, nodeName, results) { |
Ivan Udovichenko | d1bd28c | 2018-10-02 12:41:04 +0300 | [diff] [blame^] | 37 | def common = new com.mirantis.mk.Common() |
| 38 | def http = new com.mirantis.mk.Http() |
| 39 | |
Vasyl Saienko | 4e8ec64 | 2018-09-17 10:08:08 +0000 | [diff] [blame] | 40 | // Yes, we do not care of performance and will create at least 4 requests per each result |
| 41 | def requestData = [:] |
| 42 | |
| 43 | def cloudId |
| 44 | def nodeId |
| 45 | |
| 46 | // Let's take a look, may be our minion is already presented on the dashboard |
| 47 | // Get available environments |
| 48 | environments = common.parseJSON(http.sendHttpGetRequest("${apiUrl}/environment/")) |
| 49 | for (environment in environments) { |
| 50 | if (environment['name'] == cloudName) { |
| 51 | cloudId = environment['uuid'] |
| 52 | break |
| 53 | } |
| 54 | } |
| 55 | // Cloud wasn't presented, let's create it |
| 56 | if (! cloudId ) { |
| 57 | // Create cloud |
| 58 | resuestData['name'] = cloudName |
| 59 | cloudId = common.parseJSON(http.sendHttpPostRequest("${apiUrl}/environment/", requestData))['env']['uuid'] |
| 60 | |
| 61 | // And the node |
| 62 | // It was done here to reduce count of requests to the api. |
| 63 | // Because if there was not cloud presented on the dashboard, then the node was not presented as well. |
| 64 | requestData['nodes'] = [nodeName] |
| 65 | nodeId = common.parseJSON(http.sendHttpPutRequest("${apiUrl}/environment/${cloudId}/nodes/", requestData))['uuid'] |
| 66 | } |
| 67 | |
| 68 | if (! nodeId ) { |
| 69 | // Get available nodes in our environment |
| 70 | nodes = common.parseJSON(http.sendHttpGetRequest("${apiUrl}/environment/${cloudId}/nodes/")) |
| 71 | for (node in nodes) { |
| 72 | if (node['name'] == nodeName) { |
| 73 | nodeId = node['id'] |
| 74 | break |
| 75 | } |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | // Node wasn't presented, let's create it |
| 80 | if (! nodeId ) { |
| 81 | // Create node |
| 82 | requestData['nodes'] = [nodeName] |
| 83 | nodeId = common.parseJSON(http.sendHttpPutRequest("${apiUrl}/environment/${cloudId}/nodes/", requestData))['uuid'] |
| 84 | } |
| 85 | |
| 86 | // Get report_id |
| 87 | requestData['env_uuid'] = cloudId |
| 88 | def reportId = common.parseJSON(http.sendHttpPostRequest("${apiUrl}/reports/openscap/", requestData))['report']['uuid'] |
| 89 | |
| 90 | // Upload results |
| 91 | requestData['results'] = results |
| 92 | requestData['node_name'] = nodeName |
| 93 | http.sendHttpPutRequest("${apiUrl}/reports/openscap/${reportId}/", requestData) |
| 94 | } |
| 95 | |
| 96 | |
| 97 | node('python') { |
| 98 | def pepperEnv = 'pepperEnv' |
| 99 | |
| 100 | // XCCDF related variables |
| 101 | def benchmarksAndProfilesArray = XCCDF_BENCHMARKS.tokenize(';') |
| 102 | def benchmarksDir = XCCDF_BENCHMARKS_DIR ?: '/usr/share/xccdf-benchmarks/mirantis/' |
| 103 | def xccdfVersion = XCCDF_VERSION ?: '1.2' |
| 104 | def xccdfTailoringId = XCCDF_TAILORING_ID ?: 'None' |
| 105 | def targetServers = TARGET_SERVERS ?: '*' |
| 106 | |
| 107 | def salt = new com.mirantis.mk.Salt() |
| 108 | def python = new com.mirantis.mk.Python() |
| 109 | def common = new com.mirantis.mk.Common() |
| 110 | def http = new com.mirantis.mk.Http() |
| 111 | |
| 112 | // To have an ability to work in heavy concurrency conditions |
| 113 | def scanUUID = UUID.randomUUID().toString() |
| 114 | |
| 115 | def artifactsArchiveName = "openscap-${scanUUID}.zip" |
Ivan Udovichenko | d1bd28c | 2018-10-02 12:41:04 +0300 | [diff] [blame^] | 116 | def resultsBaseDir = "/var/log/openscap/${scanUUID}" |
| 117 | def artifactsDir = "openscap" |
Vasyl Saienko | 4e8ec64 | 2018-09-17 10:08:08 +0000 | [diff] [blame] | 118 | |
| 119 | def liveMinions |
| 120 | |
Ivan Udovichenko | d1bd28c | 2018-10-02 12:41:04 +0300 | [diff] [blame^] | 121 | |
Vasyl Saienko | 4e8ec64 | 2018-09-17 10:08:08 +0000 | [diff] [blame] | 122 | stage ('Setup virtualenv for Pepper') { |
| 123 | python.setupPepperVirtualenv(pepperEnv, SALT_MASTER_URL, SALT_MASTER_CREDENTIALS) |
| 124 | } |
| 125 | |
| 126 | stage ('Run openscap xccdf evaluation and attempt to upload the results to a dashboard') { |
| 127 | liveMinions = salt.getMinions(pepperEnv, targetServers) |
| 128 | |
| 129 | if (liveMinions.isEmpty()) { |
| 130 | throw new Exception('There are no alive minions') |
| 131 | } |
| 132 | |
| 133 | common.infoMsg("Scan UUID: ${scanUUID}") |
| 134 | |
Ivan Udovichenko | d1bd28c | 2018-10-02 12:41:04 +0300 | [diff] [blame^] | 135 | // Clean all results before proceeding with results from every minion |
| 136 | dir(artifactsDir) { |
| 137 | deleteDir() |
| 138 | } |
| 139 | |
Vasyl Saienko | 4e8ec64 | 2018-09-17 10:08:08 +0000 | [diff] [blame] | 140 | for (minion in liveMinions) { |
| 141 | |
| 142 | // Iterate oscap evaluation over the benchmarks |
| 143 | for (benchmark in benchmarksAndProfilesArray) { |
| 144 | def (benchmarkFilePath, profile) = benchmark.tokenize(',').collect({it.trim()}) |
| 145 | |
| 146 | // Remove extension from the benchmark name |
| 147 | def benchmarkPathWithoutExtension = benchmarkFilePath.replaceFirst('[.][^.]+$', '') |
Ivan Udovichenko | d1bd28c | 2018-10-02 12:41:04 +0300 | [diff] [blame^] | 148 | |
| 149 | // Get benchmark name |
| 150 | def benchmarkName = benchmarkPathWithoutExtension.tokenize('/')[-1] |
| 151 | |
Vasyl Saienko | 4e8ec64 | 2018-09-17 10:08:08 +0000 | [diff] [blame] | 152 | // And build resultsDir based on this path |
| 153 | def resultsDir = "${resultsBaseDir}/${benchmarkPathWithoutExtension}" |
| 154 | |
| 155 | def benchmarkFile = "${benchmarksDir}${benchmarkFilePath}" |
| 156 | |
Ivan Udovichenko | d1bd28c | 2018-10-02 12:41:04 +0300 | [diff] [blame^] | 157 | def nodeShortName = minion.tokenize('.')[0] |
| 158 | |
| 159 | def archiveName = "${scanUUID}_${nodeShortName}_${benchmarkName}.tar" |
| 160 | |
Vasyl Saienko | 4e8ec64 | 2018-09-17 10:08:08 +0000 | [diff] [blame] | 161 | // Evaluate the benchmark |
| 162 | salt.runSaltProcessStep(pepperEnv, minion, 'oscap.eval', [ |
| 163 | 'xccdf', benchmarkFile, "results_dir=${resultsDir}", |
| 164 | "profile=${profile}", "xccdf_version=${xccdfVersion}", |
| 165 | "tailoring_id=${xccdfTailoringId}" |
| 166 | ]) |
| 167 | |
Ivan Udovichenko | d1bd28c | 2018-10-02 12:41:04 +0300 | [diff] [blame^] | 168 | salt.cmdRun(pepperEnv, minion, "tar -cf /tmp/${archiveName} -C ${resultsBaseDir} .") |
| 169 | fileContents = salt.cmdRun(pepperEnv, minion, "cat /tmp/${archiveName}", true, null, false)['return'][0].values()[0].replaceAll('Salt command execution success', '') |
| 170 | |
| 171 | sh "mkdir -p ${artifactsDir}/${scanUUID}/${nodeShortName}" |
| 172 | writeFile file: "${archiveName}", text: fileContents |
| 173 | sh "tar --strip-components 1 -xf ${archiveName} --directory ${artifactsDir}/${scanUUID}/${nodeShortName}; rm -f ${archiveName}" |
| 174 | |
| 175 | // Remove archive which is not needed anymore |
| 176 | salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', "/tmp/${archiveName}") |
| 177 | |
Vasyl Saienko | 4e8ec64 | 2018-09-17 10:08:08 +0000 | [diff] [blame] | 178 | // Attempt to upload the scanning results to the dashboard |
| 179 | if (UPLOAD_TO_DASHBOARD.toBoolean()) { |
| 180 | if (common.validInputParam('DASHBOARD_API_URL')) { |
| 181 | def cloudName = salt.getGrain(pepperEnv, minion, 'domain')['return'][0].values()[0].values()[0] |
| 182 | uploadResultToDashboard(DASHBOARD_API_URL, cloudName, minion, salt.getFileContent(pepperEnv, minion, "${resultsDir}/results.json")) |
| 183 | } else { |
| 184 | throw new Exception('Uploading to the dashboard is enabled but the DASHBOARD_API_URL was not set') |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | } |
Ivan Udovichenko | d1bd28c | 2018-10-02 12:41:04 +0300 | [diff] [blame^] | 189 | |
| 190 | // Prepare archive |
| 191 | sh "tar -cJf ${artifactsDir}.tar.xz ${artifactsDir}" |
| 192 | |
| 193 | // Archive the build output artifacts |
| 194 | archiveArtifacts artifacts: "*.xz" |
Vasyl Saienko | 4e8ec64 | 2018-09-17 10:08:08 +0000 | [diff] [blame] | 195 | } |
| 196 | |
| 197 | /* // Will be implemented later |
| 198 | stage ('Attempt to upload results to an artifactory') { |
| 199 | if (common.validInputParam('ARTIFACTORY_URL')) { |
| 200 | for (minion in liveMinions) { |
| 201 | def destDir = "${artifactsDir}/${minion}" |
| 202 | def archiveName = "openscap-${scanUUID}.tar.gz" |
| 203 | def tempArchive = "/tmp/${archiveName}" |
| 204 | def destination = "${destDir}/${archiveName}" |
| 205 | |
| 206 | dir(destDir) { |
| 207 | // Archive scanning results on the remote target |
| 208 | salt.runSaltProcessStep(pepperEnv, minion, 'archive.tar', ['czf', tempArchive, resultsBaseDir]) |
| 209 | |
| 210 | // Get it content and save it |
| 211 | writeFile file: destination, text: salt.getFileContent(pepperEnv, minion, tempArchive) |
| 212 | |
| 213 | // Remove scanning results and the temp archive on the remote target |
| 214 | salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', resultsBaseDir) |
| 215 | salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', tempArchive) |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | def artifactory = new com.mirantis.mcp.MCPArtifactory() |
| 220 | def artifactoryName = 'mcp-ci' |
| 221 | def artifactoryRepo = ARTIFACTORY_REPO ?: 'binary-dev-local' |
| 222 | def artifactoryNamespace = ARTIFACTORY_NAMESPACE ?: 'mirantis/openscap' |
| 223 | def artifactoryServer = Artifactory.server(artifactoryName) |
| 224 | def publishInfo = true |
| 225 | def buildInfo = Artifactory.newBuildInfo() |
| 226 | def zipName = "${env.WORKSPACE}/openscap/${scanUUID}/results.zip" |
| 227 | |
| 228 | // Zip scan results |
| 229 | zip zipFile: zipName, archive: false, dir: artifactsDir |
| 230 | |
| 231 | // Mandatory and additional properties |
| 232 | def properties = artifactory.getBinaryBuildProperties([ |
| 233 | "scanUuid=${scanUUID}", |
| 234 | "project=openscap" |
| 235 | ]) |
| 236 | |
| 237 | // Build Artifactory spec object |
| 238 | def uploadSpec = """{ |
| 239 | "files": |
| 240 | [ |
| 241 | { |
| 242 | "pattern": "${zipName}", |
| 243 | "target": "${artifactoryRepo}/${artifactoryNamespace}/openscap", |
| 244 | "props": "${properties}" |
| 245 | } |
| 246 | ] |
| 247 | }""" |
| 248 | |
| 249 | // Upload artifacts to the given Artifactory |
| 250 | artifactory.uploadBinariesToArtifactory(artifactoryServer, buildInfo, uploadSpec, publishInfo) |
| 251 | |
| 252 | } else { |
| 253 | common.warningMsg('ARTIFACTORY_URL was not given, skip uploading to artifactory') |
| 254 | } |
| 255 | } |
| 256 | */ |
| 257 | |
| 258 | } |