blob: b886467cb49d3906e7655508cedda45702b8c96e [file] [log] [blame]
Vasyl Saienko4e8ec642018-09-17 10:08:08 +00001/**
2 *
3 * Run openscap xccdf evaluation on given nodes
4 *
5 * Expected parametes:
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +03006 * OPENSCAP_TEST_TYPE Type of OpenSCAP evaluation to run, either 'xccdf' or 'oval'
Vasyl Saienko4e8ec642018-09-17 10:08:08 +00007 * SALT_MASTER_URL Full Salt API address.
8 * SALT_MASTER_CREDENTIALS Credentials to the Salt API.
9 *
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +030010 * XCCDF_BENCHMARKS_DIR Base directory for XCCDF benchmarks (default /usr/share/xccdf-benchmarks/mirantis/)
11 * or OVAL devinitions (default /usr/share/oval-definitions/mirantis/)
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000012 * XCCDF_BENCHMARKS List of pairs XCCDF benchmark filename and corresponding profile separated with ','
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +030013 * these pairs are separated with semicolon
14 * (e.g. manila/openstack_manila-xccdf.xml,profilename;horizon/openstack_horizon-xccdf.xml,profile).
15 * For OVAL definitions, paths to OVAL definition files separated by semicolon, profile is ignored.
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000016 * XCCDF_VERSION The XCCDF version (default 1.2)
17 * XCCDF_TAILORING_ID The tailoring id (default None)
18 *
19 * TARGET_SERVERS The target Salt nodes (default *)
20 *
21 * ARTIFACTORY_URL The artifactory URL
22 * ARTIFACTORY_NAMESPACE The artifactory namespace (default 'mirantis/openscap')
23 * ARTIFACTORY_REPO The artifactory repo (default 'binary-dev-local')
24 *
25 * UPLOAD_TO_DASHBOARD Boolean. Upload results to the WORP or not
26 * DASHBOARD_API_URL The WORP api base url. Mandatory if UPLOAD_TO_DASHBOARD is true
27 */
28
29
30
31/**
32 * Upload results to the `WORP` dashboard
33 *
34 * @param apiUrl The base dashboard api url
35 * @param cloudName The cloud name (mostly, the given node's domain name)
36 * @param nodeName The node name
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030037 * @param reportType Type of the report to create/use, either 'openscap' or 'cve'
38 * @param reportId Report Id to re-use, if empty report will be created
39 * @param results The scanning results as a json file content (string)
40 * @return reportId The Id of the report created if incoming reportId was empty, otherwise incoming reportId
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000041 */
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030042def uploadResultToDashboard(apiUrl, cloudName, nodeName, reportType, reportId, results) {
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +030043 def common = new com.mirantis.mk.Common()
44 def http = new com.mirantis.mk.Http()
45
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000046 // Yes, we do not care of performance and will create at least 4 requests per each result
47 def requestData = [:]
48
49 def cloudId
50 def nodeId
51
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030052 def worpApi = [:]
53 worpApi["url"] = apiUrl
54
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000055 // Let's take a look, may be our minion is already presented on the dashboard
56 // Get available environments
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030057 common.infoMsg("Making GET to ${worpApi.url}/environment/")
58 environments = http.restGet(worpApi, "/environment/")
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000059 for (environment in environments) {
60 if (environment['name'] == cloudName) {
61 cloudId = environment['uuid']
62 break
63 }
64 }
65 // Cloud wasn't presented, let's create it
66 if (! cloudId ) {
67 // Create cloud
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030068 requestData = [:]
69 requestData['name'] = cloudName
70 common.infoMsg("Making POST to ${worpApi.url}/environment/ with ${requestData}")
71 cloudId = http.restPost(worpApi, "/environment/", requestData)['env']['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000072
73 // And the node
74 // It was done here to reduce count of requests to the api.
75 // Because if there was not cloud presented on the dashboard, then the node was not presented as well.
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030076 requestData = [:]
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000077 requestData['nodes'] = [nodeName]
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030078 common.infoMsg("Making PUT to ${worpApi.url}/environment/${cloudId}/nodes/ with ${requestData}")
79 nodeId = http.restCall(worpApi, "/environment/${cloudId}/nodes/", "PUT", requestData)['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000080 }
81
82 if (! nodeId ) {
83 // Get available nodes in our environment
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030084 common.infoMsg("Making GET to ${worpApi.url}/environment/${cloudId}/nodes/")
85 nodes = http.restGet(worpApi, "/environment/${cloudId}/nodes/")
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000086 for (node in nodes) {
87 if (node['name'] == nodeName) {
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030088 nodeId = node['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000089 break
90 }
91 }
92 }
93
94 // Node wasn't presented, let's create it
95 if (! nodeId ) {
96 // Create node
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030097 requestData = [:]
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000098 requestData['nodes'] = [nodeName]
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030099 common.infoMsg("Making PUT to ${worpApi.url}/environment/${cloudId}/nodes/ with ${requestData}")
100 nodeId = http.restCall(worpApi, "/environment/${cloudId}/nodes/", "PUT", requestData)['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000101 }
102
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300103 // Create report if needed
104 if (! reportId ) {
105 requestData = [:]
106 requestData['env_uuid'] = cloudId
107 common.infoMsg("Making POST to ${worpApi.url}/reports/${reportType}/ with ${requestData}")
108 reportId = http.restPost(worpApi, "/reports/${reportType}/", requestData)['report']['uuid']
109 }
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000110
111 // Upload results
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300112 // NOTE(pas-ha) results should already be a dict with 'results' key
113 requestData = common.parseJSON(results)
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000114 requestData['node_name'] = nodeName
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300115 common.infoMsg("First result in results to PUT is ${requestData['results'][0]}")
116 // NOTE(pas-ha) not logging whole results to be sent, is too large and just spams the logs
117 common.infoMsg("Making PUT to ${worpApi.url}/reports/${reportType}/${reportId}/ with node name ${requestData['node_name']} and results")
118 http.restCall(worpApi, "/reports/${reportType}/${reportId}/", "PUT", requestData)
119 return reportId
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000120}
121
122
123node('python') {
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000124 def salt = new com.mirantis.mk.Salt()
125 def python = new com.mirantis.mk.Python()
126 def common = new com.mirantis.mk.Common()
127 def http = new com.mirantis.mk.Http()
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300128 def validate = new com.mirantis.mcp.Validate()
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000129
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +0300130 def pepperEnv = 'pepperEnv'
131
132 def benchmarkType = OPENSCAP_TEST_TYPE ?: 'xccdf'
133 def reportType
134 def benchmarksDir
135
136 switch (benchmarkType) {
137 case 'xccdf':
138 reportType = 'openscap';
139 benchmarksDir = XCCDF_BENCHMARKS_DIR ?: '/usr/share/xccdf-benchmarks/mirantis/';
140 break;
141 case 'oval':
142 reportType = 'cve';
143 benchmarksDir = XCCDF_BENCHMARKS_DIR ?: '/usr/share/oval-definitions/mirantis/';
144 break;
145 default:
146 throw new Exception('Unsupported value for OPENSCAP_TEST_TYPE, must be "oval" or "xccdf".')
147 }
148 // XCCDF related variables
149 def benchmarksAndProfilesArray = XCCDF_BENCHMARKS.tokenize(';')
150 def xccdfVersion = XCCDF_VERSION ?: '1.2'
151 def xccdfTailoringId = XCCDF_TAILORING_ID ?: 'None'
152 def targetServers = TARGET_SERVERS ?: '*'
153
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000154 // To have an ability to work in heavy concurrency conditions
155 def scanUUID = UUID.randomUUID().toString()
156
157 def artifactsArchiveName = "openscap-${scanUUID}.zip"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300158 def resultsBaseDir = "/var/log/openscap/${scanUUID}"
159 def artifactsDir = "openscap"
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000160
161 def liveMinions
162
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300163
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000164 stage ('Setup virtualenv for Pepper') {
165 python.setupPepperVirtualenv(pepperEnv, SALT_MASTER_URL, SALT_MASTER_CREDENTIALS)
166 }
167
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +0300168 stage ('Run openscap evaluation and attempt to upload the results to a dashboard') {
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000169 liveMinions = salt.getMinions(pepperEnv, targetServers)
170
171 if (liveMinions.isEmpty()) {
172 throw new Exception('There are no alive minions')
173 }
174
175 common.infoMsg("Scan UUID: ${scanUUID}")
176
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300177 // Clean all results before proceeding with results from every minion
178 dir(artifactsDir) {
179 deleteDir()
180 }
181
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300182 def reportId
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300183 def lastError
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300184 // Iterate oscap evaluation over the benchmarks
185 for (benchmark in benchmarksAndProfilesArray) {
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300186 def (benchmarkFilePath, profileName) = benchmark.tokenize(',').collect({it.trim()})
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000187
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300188 // Remove extension from the benchmark name
189 def benchmarkPathWithoutExtension = benchmarkFilePath.replaceFirst('[.][^.]+$', '')
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000190
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300191 // Get benchmark name
192 def benchmarkName = benchmarkPathWithoutExtension.tokenize('/')[-1]
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300193
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300194 // And build resultsDir based on this path
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300195 def resultsDir = "${resultsBaseDir}/${benchmarkName}"
196 if (profileName) {
197 resultsDir = "${resultsDir}/${profileName}"
198 }
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300199
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300200 def benchmarkFile = "${benchmarksDir}${benchmarkFilePath}"
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000201
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300202 // Evaluate the benchmark on all minions at once
203 salt.runSaltProcessStep(pepperEnv, targetServers, 'oscap.eval', [
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +0300204 benchmarkType, benchmarkFile, "results_dir=${resultsDir}",
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300205 "profile=${profileName}", "xccdf_version=${xccdfVersion}",
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300206 "tailoring_id=${xccdfTailoringId}"
207 ])
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000208
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300209 salt.cmdRun(pepperEnv, targetServers, "rm -f /tmp/${scanUUID}.tar.xz; tar -cJf /tmp/${scanUUID}.tar.xz -C ${resultsBaseDir} .")
210
211 // fetch and store results one by one
212 for (minion in liveMinions) {
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300213 def nodeShortName = minion.tokenize('.')[0]
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300214 def localResultsDir = "${artifactsDir}/${scanUUID}/${nodeShortName}"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300215
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300216 fileContentBase64 = validate.getFileContentEncoded(pepperEnv, minion, "/tmp/${scanUUID}.tar.xz")
217 writeFile file: "${scanUUID}.base64", text: fileContentBase64
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300218
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300219 sh "mkdir -p ${localResultsDir}"
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300220 sh "base64 -d ${scanUUID}.base64 | tar -xJ --strip-components 1 --directory ${localResultsDir}"
221 sh "rm -f ${scanUUID}.base64"
222 }
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300223
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300224 // Remove archives which is not needed anymore
225 salt.runSaltProcessStep(pepperEnv, targetServers, 'file.remove', "/tmp/${scanUUID}.tar.xz")
226
227 // publish results one by one
228 for (minion in liveMinions) {
229 def nodeShortName = minion.tokenize('.')[0]
230 def benchmarkResultsDir = "${artifactsDir}/${scanUUID}/${nodeShortName}/${benchmarkName}"
231 if (profileName) {
232 benchmarkResultsDir = "${benchmarkResultsDir}/${profileName}"
233 }
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300234
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000235 // Attempt to upload the scanning results to the dashboard
236 if (UPLOAD_TO_DASHBOARD.toBoolean()) {
237 if (common.validInputParam('DASHBOARD_API_URL')) {
238 def cloudName = salt.getGrain(pepperEnv, minion, 'domain')['return'][0].values()[0].values()[0]
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300239 try {
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300240 def nodeResults = readFile "${benchmarkResultsDir}/results.json"
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300241 reportId = uploadResultToDashboard(DASHBOARD_API_URL, cloudName, minion, reportType, reportId, nodeResults)
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300242 common.infoMsg("Report ID is ${reportId}.")
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300243 } catch (Exception e) {
244 lastError = e
245 }
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000246 } else {
247 throw new Exception('Uploading to the dashboard is enabled but the DASHBOARD_API_URL was not set')
248 }
249 }
250 }
251 }
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300252
253 // Prepare archive
254 sh "tar -cJf ${artifactsDir}.tar.xz ${artifactsDir}"
255
256 // Archive the build output artifacts
257 archiveArtifacts artifacts: "*.xz"
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300258 if (lastError) {
259 common.infoMsg('Uploading some results to the dashboard report ${reportId} failed. Raising last error.')
260 throw lastError
261 }
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000262 }
263
264/* // Will be implemented later
265 stage ('Attempt to upload results to an artifactory') {
266 if (common.validInputParam('ARTIFACTORY_URL')) {
267 for (minion in liveMinions) {
268 def destDir = "${artifactsDir}/${minion}"
269 def archiveName = "openscap-${scanUUID}.tar.gz"
270 def tempArchive = "/tmp/${archiveName}"
271 def destination = "${destDir}/${archiveName}"
272
273 dir(destDir) {
274 // Archive scanning results on the remote target
275 salt.runSaltProcessStep(pepperEnv, minion, 'archive.tar', ['czf', tempArchive, resultsBaseDir])
276
277 // Get it content and save it
278 writeFile file: destination, text: salt.getFileContent(pepperEnv, minion, tempArchive)
279
280 // Remove scanning results and the temp archive on the remote target
281 salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', resultsBaseDir)
282 salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', tempArchive)
283 }
284 }
285
286 def artifactory = new com.mirantis.mcp.MCPArtifactory()
287 def artifactoryName = 'mcp-ci'
288 def artifactoryRepo = ARTIFACTORY_REPO ?: 'binary-dev-local'
289 def artifactoryNamespace = ARTIFACTORY_NAMESPACE ?: 'mirantis/openscap'
290 def artifactoryServer = Artifactory.server(artifactoryName)
291 def publishInfo = true
292 def buildInfo = Artifactory.newBuildInfo()
293 def zipName = "${env.WORKSPACE}/openscap/${scanUUID}/results.zip"
294
295 // Zip scan results
296 zip zipFile: zipName, archive: false, dir: artifactsDir
297
298 // Mandatory and additional properties
299 def properties = artifactory.getBinaryBuildProperties([
300 "scanUuid=${scanUUID}",
301 "project=openscap"
302 ])
303
304 // Build Artifactory spec object
305 def uploadSpec = """{
306 "files":
307 [
308 {
309 "pattern": "${zipName}",
310 "target": "${artifactoryRepo}/${artifactoryNamespace}/openscap",
311 "props": "${properties}"
312 }
313 ]
314 }"""
315
316 // Upload artifacts to the given Artifactory
317 artifactory.uploadBinariesToArtifactory(artifactoryServer, buildInfo, uploadSpec, publishInfo)
318
319 } else {
320 common.warningMsg('ARTIFACTORY_URL was not given, skip uploading to artifactory')
321 }
322 }
323*/
324
325}