blob: 2ec54e11744f6cedf9864edabc5a47c8217326de [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)
Ivan Udovichenko21892e62018-12-04 13:16:45 +030018 * XCCDF_CPE CPE dictionary or language for applicability checks (default None)
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000019 *
20 * TARGET_SERVERS The target Salt nodes (default *)
21 *
22 * ARTIFACTORY_URL The artifactory URL
23 * ARTIFACTORY_NAMESPACE The artifactory namespace (default 'mirantis/openscap')
24 * ARTIFACTORY_REPO The artifactory repo (default 'binary-dev-local')
25 *
26 * UPLOAD_TO_DASHBOARD Boolean. Upload results to the WORP or not
27 * DASHBOARD_API_URL The WORP api base url. Mandatory if UPLOAD_TO_DASHBOARD is true
Pavlo Shchelokovskyy7373b712018-12-26 13:17:32 +000028 * CLOUD_NAME Name of the cloud to post results to dashboard for.
29 If not specified, the internal cloud name resolved from Salt master will be used.
30 Suitable for CI-like use case.
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000031 */
32
33
34
35/**
36 * Upload results to the `WORP` dashboard
37 *
38 * @param apiUrl The base dashboard api url
39 * @param cloudName The cloud name (mostly, the given node's domain name)
40 * @param nodeName The node name
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030041 * @param reportType Type of the report to create/use, either 'openscap' or 'cve'
42 * @param reportId Report Id to re-use, if empty report will be created
Pavlo Shchelokovskyyd5514192018-12-13 18:52:16 +000043 * @param results The scanning results as a XML file content (string)
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030044 * @return reportId The Id of the report created if incoming reportId was empty, otherwise incoming reportId
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000045 */
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030046def uploadResultToDashboard(apiUrl, cloudName, nodeName, reportType, reportId, results) {
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +030047 def common = new com.mirantis.mk.Common()
Pavlo Shchelokovskyyd5514192018-12-13 18:52:16 +000048 def mcp_common = new com.mirantis.mcp.Common()
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +030049 def http = new com.mirantis.mk.Http()
50
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000051 // Yes, we do not care of performance and will create at least 4 requests per each result
52 def requestData = [:]
53
54 def cloudId
55 def nodeId
56
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030057 def worpApi = [:]
58 worpApi["url"] = apiUrl
59
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000060 // Let's take a look, may be our minion is already presented on the dashboard
61 // Get available environments
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030062 common.infoMsg("Making GET to ${worpApi.url}/environment/")
63 environments = http.restGet(worpApi, "/environment/")
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000064 for (environment in environments) {
65 if (environment['name'] == cloudName) {
66 cloudId = environment['uuid']
67 break
68 }
69 }
70 // Cloud wasn't presented, let's create it
71 if (! cloudId ) {
72 // Create cloud
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030073 requestData = [:]
74 requestData['name'] = cloudName
75 common.infoMsg("Making POST to ${worpApi.url}/environment/ with ${requestData}")
76 cloudId = http.restPost(worpApi, "/environment/", requestData)['env']['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000077
78 // And the node
79 // It was done here to reduce count of requests to the api.
80 // 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 +030081 requestData = [:]
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000082 requestData['nodes'] = [nodeName]
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030083 common.infoMsg("Making PUT to ${worpApi.url}/environment/${cloudId}/nodes/ with ${requestData}")
84 nodeId = http.restCall(worpApi, "/environment/${cloudId}/nodes/", "PUT", requestData)['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000085 }
86
87 if (! nodeId ) {
88 // Get available nodes in our environment
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030089 common.infoMsg("Making GET to ${worpApi.url}/environment/${cloudId}/nodes/")
90 nodes = http.restGet(worpApi, "/environment/${cloudId}/nodes/")
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000091 for (node in nodes) {
92 if (node['name'] == nodeName) {
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030093 nodeId = node['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000094 break
95 }
96 }
97 }
98
99 // Node wasn't presented, let's create it
100 if (! nodeId ) {
101 // Create node
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300102 requestData = [:]
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000103 requestData['nodes'] = [nodeName]
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300104 common.infoMsg("Making PUT to ${worpApi.url}/environment/${cloudId}/nodes/ with ${requestData}")
105 nodeId = http.restCall(worpApi, "/environment/${cloudId}/nodes/", "PUT", requestData)['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000106 }
107
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300108 // Create report if needed
109 if (! reportId ) {
110 requestData = [:]
111 requestData['env_uuid'] = cloudId
112 common.infoMsg("Making POST to ${worpApi.url}/reports/${reportType}/ with ${requestData}")
113 reportId = http.restPost(worpApi, "/reports/${reportType}/", requestData)['report']['uuid']
114 }
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000115
116 // Upload results
Pavlo Shchelokovskyyd5514192018-12-13 18:52:16 +0000117 requestData = [node_name: nodeName, xmlgzb64: mcp_common.zipBase64(results)]
118 common.infoMsg("Making PUT to ${worpApi.url}/reports/${reportType}/${reportId}/ with node name ${requestData['node_name']} and encoded raw results")
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300119 http.restCall(worpApi, "/reports/${reportType}/${reportId}/", "PUT", requestData)
120 return reportId
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000121}
122
123
124node('python') {
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000125 def salt = new com.mirantis.mk.Salt()
126 def python = new com.mirantis.mk.Python()
127 def common = new com.mirantis.mk.Common()
128 def http = new com.mirantis.mk.Http()
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300129 def validate = new com.mirantis.mcp.Validate()
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000130
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +0300131 def pepperEnv = 'pepperEnv'
132
133 def benchmarkType = OPENSCAP_TEST_TYPE ?: 'xccdf'
134 def reportType
135 def benchmarksDir
136
137 switch (benchmarkType) {
138 case 'xccdf':
139 reportType = 'openscap';
140 benchmarksDir = XCCDF_BENCHMARKS_DIR ?: '/usr/share/xccdf-benchmarks/mirantis/';
141 break;
142 case 'oval':
143 reportType = 'cve';
144 benchmarksDir = XCCDF_BENCHMARKS_DIR ?: '/usr/share/oval-definitions/mirantis/';
145 break;
146 default:
147 throw new Exception('Unsupported value for OPENSCAP_TEST_TYPE, must be "oval" or "xccdf".')
148 }
149 // XCCDF related variables
150 def benchmarksAndProfilesArray = XCCDF_BENCHMARKS.tokenize(';')
151 def xccdfVersion = XCCDF_VERSION ?: '1.2'
152 def xccdfTailoringId = XCCDF_TAILORING_ID ?: 'None'
Ivan Udovichenko21892e62018-12-04 13:16:45 +0300153 def xccdfCPE = XCCDF_CPE ?: ''
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +0300154 def targetServers = TARGET_SERVERS ?: '*'
155
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000156 // To have an ability to work in heavy concurrency conditions
157 def scanUUID = UUID.randomUUID().toString()
158
159 def artifactsArchiveName = "openscap-${scanUUID}.zip"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300160 def resultsBaseDir = "/var/log/openscap/${scanUUID}"
161 def artifactsDir = "openscap"
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000162
163 def liveMinions
164
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300165
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000166 stage ('Setup virtualenv for Pepper') {
167 python.setupPepperVirtualenv(pepperEnv, SALT_MASTER_URL, SALT_MASTER_CREDENTIALS)
168 }
169
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +0300170 stage ('Run openscap evaluation and attempt to upload the results to a dashboard') {
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000171 liveMinions = salt.getMinions(pepperEnv, targetServers)
172
173 if (liveMinions.isEmpty()) {
174 throw new Exception('There are no alive minions')
175 }
176
177 common.infoMsg("Scan UUID: ${scanUUID}")
178
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300179 // Clean all results before proceeding with results from every minion
180 dir(artifactsDir) {
181 deleteDir()
182 }
183
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300184 def reportId
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300185 def lastError
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300186 // Iterate oscap evaluation over the benchmarks
187 for (benchmark in benchmarksAndProfilesArray) {
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300188 def (benchmarkFilePath, profileName) = benchmark.tokenize(',').collect({it.trim()})
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000189
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300190 // Remove extension from the benchmark name
191 def benchmarkPathWithoutExtension = benchmarkFilePath.replaceFirst('[.][^.]+$', '')
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000192
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300193 // Get benchmark name
194 def benchmarkName = benchmarkPathWithoutExtension.tokenize('/')[-1]
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300195
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300196 // And build resultsDir based on this path
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300197 def resultsDir = "${resultsBaseDir}/${benchmarkName}"
198 if (profileName) {
199 resultsDir = "${resultsDir}/${profileName}"
200 }
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300201
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300202 def benchmarkFile = "${benchmarksDir}${benchmarkFilePath}"
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000203
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300204 // Evaluate the benchmark on all minions at once
205 salt.runSaltProcessStep(pepperEnv, targetServers, 'oscap.eval', [
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +0300206 benchmarkType, benchmarkFile, "results_dir=${resultsDir}",
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300207 "profile=${profileName}", "xccdf_version=${xccdfVersion}",
Ivan Udovichenko21892e62018-12-04 13:16:45 +0300208 "tailoring_id=${xccdfTailoringId}", "cpe=${xccdfCPE}"
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300209 ])
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000210
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300211 salt.cmdRun(pepperEnv, targetServers, "rm -f /tmp/${scanUUID}.tar.xz; tar -cJf /tmp/${scanUUID}.tar.xz -C ${resultsBaseDir} .")
212
213 // fetch and store results one by one
214 for (minion in liveMinions) {
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300215 def nodeShortName = minion.tokenize('.')[0]
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300216 def localResultsDir = "${artifactsDir}/${scanUUID}/${nodeShortName}"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300217
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300218 fileContentBase64 = validate.getFileContentEncoded(pepperEnv, minion, "/tmp/${scanUUID}.tar.xz")
219 writeFile file: "${scanUUID}.base64", text: fileContentBase64
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300220
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300221 sh "mkdir -p ${localResultsDir}"
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300222 sh "base64 -d ${scanUUID}.base64 | tar -xJ --strip-components 1 --directory ${localResultsDir}"
223 sh "rm -f ${scanUUID}.base64"
224 }
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300225
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300226 // Remove archives which is not needed anymore
227 salt.runSaltProcessStep(pepperEnv, targetServers, 'file.remove', "/tmp/${scanUUID}.tar.xz")
228
229 // publish results one by one
230 for (minion in liveMinions) {
231 def nodeShortName = minion.tokenize('.')[0]
232 def benchmarkResultsDir = "${artifactsDir}/${scanUUID}/${nodeShortName}/${benchmarkName}"
233 if (profileName) {
234 benchmarkResultsDir = "${benchmarkResultsDir}/${profileName}"
235 }
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300236
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000237 // Attempt to upload the scanning results to the dashboard
238 if (UPLOAD_TO_DASHBOARD.toBoolean()) {
239 if (common.validInputParam('DASHBOARD_API_URL')) {
Pavlo Shchelokovskyy7373b712018-12-26 13:17:32 +0000240 def cloudName
241 if (common.validInputParam('CLOUD_NAME')) {
242 cloudName = CLOUD_NAME
243 } else {
244 cloudName = salt.getGrain(pepperEnv, minion, 'domain')['return'][0].values()[0].values()[0]
245 }
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300246 try {
Pavlo Shchelokovskyyd5514192018-12-13 18:52:16 +0000247 def nodeResults = readFile "${benchmarkResultsDir}/results.xml"
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300248 reportId = uploadResultToDashboard(DASHBOARD_API_URL, cloudName, minion, reportType, reportId, nodeResults)
Dmitry Teselkin1691a2e2018-10-11 12:37:44 +0300249 common.infoMsg("Report ID is ${reportId}.")
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300250 } catch (Exception e) {
251 lastError = e
252 }
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000253 } else {
254 throw new Exception('Uploading to the dashboard is enabled but the DASHBOARD_API_URL was not set')
255 }
256 }
257 }
258 }
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300259
260 // Prepare archive
261 sh "tar -cJf ${artifactsDir}.tar.xz ${artifactsDir}"
262
263 // Archive the build output artifacts
264 archiveArtifacts artifacts: "*.xz"
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300265 if (lastError) {
266 common.infoMsg('Uploading some results to the dashboard report ${reportId} failed. Raising last error.')
267 throw lastError
268 }
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000269 }
270
271/* // Will be implemented later
272 stage ('Attempt to upload results to an artifactory') {
273 if (common.validInputParam('ARTIFACTORY_URL')) {
274 for (minion in liveMinions) {
275 def destDir = "${artifactsDir}/${minion}"
276 def archiveName = "openscap-${scanUUID}.tar.gz"
277 def tempArchive = "/tmp/${archiveName}"
278 def destination = "${destDir}/${archiveName}"
279
280 dir(destDir) {
281 // Archive scanning results on the remote target
282 salt.runSaltProcessStep(pepperEnv, minion, 'archive.tar', ['czf', tempArchive, resultsBaseDir])
283
284 // Get it content and save it
285 writeFile file: destination, text: salt.getFileContent(pepperEnv, minion, tempArchive)
286
287 // Remove scanning results and the temp archive on the remote target
288 salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', resultsBaseDir)
289 salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', tempArchive)
290 }
291 }
292
293 def artifactory = new com.mirantis.mcp.MCPArtifactory()
294 def artifactoryName = 'mcp-ci'
295 def artifactoryRepo = ARTIFACTORY_REPO ?: 'binary-dev-local'
296 def artifactoryNamespace = ARTIFACTORY_NAMESPACE ?: 'mirantis/openscap'
297 def artifactoryServer = Artifactory.server(artifactoryName)
298 def publishInfo = true
299 def buildInfo = Artifactory.newBuildInfo()
300 def zipName = "${env.WORKSPACE}/openscap/${scanUUID}/results.zip"
301
302 // Zip scan results
303 zip zipFile: zipName, archive: false, dir: artifactsDir
304
305 // Mandatory and additional properties
306 def properties = artifactory.getBinaryBuildProperties([
307 "scanUuid=${scanUUID}",
308 "project=openscap"
309 ])
310
311 // Build Artifactory spec object
312 def uploadSpec = """{
313 "files":
314 [
315 {
316 "pattern": "${zipName}",
317 "target": "${artifactoryRepo}/${artifactoryNamespace}/openscap",
318 "props": "${properties}"
319 }
320 ]
321 }"""
322
323 // Upload artifacts to the given Artifactory
324 artifactory.uploadBinariesToArtifactory(artifactoryServer, buildInfo, uploadSpec, publishInfo)
325
326 } else {
327 common.warningMsg('ARTIFACTORY_URL was not given, skip uploading to artifactory')
328 }
329 }
330*/
331
332}