blob: 872ca328dc96c797421ec8e26785ea60acbb12ba [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()
128
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +0300129 def pepperEnv = 'pepperEnv'
130
131 def benchmarkType = OPENSCAP_TEST_TYPE ?: 'xccdf'
132 def reportType
133 def benchmarksDir
134
135 switch (benchmarkType) {
136 case 'xccdf':
137 reportType = 'openscap';
138 benchmarksDir = XCCDF_BENCHMARKS_DIR ?: '/usr/share/xccdf-benchmarks/mirantis/';
139 break;
140 case 'oval':
141 reportType = 'cve';
142 benchmarksDir = XCCDF_BENCHMARKS_DIR ?: '/usr/share/oval-definitions/mirantis/';
143 break;
144 default:
145 throw new Exception('Unsupported value for OPENSCAP_TEST_TYPE, must be "oval" or "xccdf".')
146 }
147 // XCCDF related variables
148 def benchmarksAndProfilesArray = XCCDF_BENCHMARKS.tokenize(';')
149 def xccdfVersion = XCCDF_VERSION ?: '1.2'
150 def xccdfTailoringId = XCCDF_TAILORING_ID ?: 'None'
151 def targetServers = TARGET_SERVERS ?: '*'
152
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000153 // To have an ability to work in heavy concurrency conditions
154 def scanUUID = UUID.randomUUID().toString()
155
156 def artifactsArchiveName = "openscap-${scanUUID}.zip"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300157 def resultsBaseDir = "/var/log/openscap/${scanUUID}"
158 def artifactsDir = "openscap"
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000159
160 def liveMinions
161
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300162
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000163 stage ('Setup virtualenv for Pepper') {
164 python.setupPepperVirtualenv(pepperEnv, SALT_MASTER_URL, SALT_MASTER_CREDENTIALS)
165 }
166
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +0300167 stage ('Run openscap evaluation and attempt to upload the results to a dashboard') {
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000168 liveMinions = salt.getMinions(pepperEnv, targetServers)
169
170 if (liveMinions.isEmpty()) {
171 throw new Exception('There are no alive minions')
172 }
173
174 common.infoMsg("Scan UUID: ${scanUUID}")
175
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300176 // Clean all results before proceeding with results from every minion
177 dir(artifactsDir) {
178 deleteDir()
179 }
180
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300181 def reportId
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300182 def lastError
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300183 // Iterate oscap evaluation over the benchmarks
184 for (benchmark in benchmarksAndProfilesArray) {
185 def (benchmarkFilePath, profile) = benchmark.tokenize(',').collect({it.trim()})
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000186
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300187 // Remove extension from the benchmark name
188 def benchmarkPathWithoutExtension = benchmarkFilePath.replaceFirst('[.][^.]+$', '')
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000189
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300190 // Get benchmark name
191 def benchmarkName = benchmarkPathWithoutExtension.tokenize('/')[-1]
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300192
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300193 // And build resultsDir based on this path
194 def resultsDir = "${resultsBaseDir}/${benchmarkPathWithoutExtension}"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300195
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300196 def benchmarkFile = "${benchmarksDir}${benchmarkFilePath}"
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000197
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300198 // Evaluate the benchmark on all minions at once
199 salt.runSaltProcessStep(pepperEnv, targetServers, 'oscap.eval', [
Pavlo Shchelokovskyy319e00f2018-10-08 17:15:44 +0300200 benchmarkType, benchmarkFile, "results_dir=${resultsDir}",
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300201 "profile=${profile}", "xccdf_version=${xccdfVersion}",
202 "tailoring_id=${xccdfTailoringId}"
203 ])
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300204 // fetch, store and publish results one by one
205 for (minion in liveMinions) {
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000206
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300207 def nodeShortName = minion.tokenize('.')[0]
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300208 def archiveName = "${scanUUID}_${nodeShortName}_${benchmarkName}.tar"
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300209 def localResultsDir = "${artifactsDir}/${scanUUID}/${nodeShortName}"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300210
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300211 // TODO(pas-ha) when using Salt >= 2017.7.0, use file.read(path) module
212 // also investigate compressing to gz/xz and reading with binary=True (for less traffic)
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300213 salt.cmdRun(pepperEnv, minion, "tar -cf /tmp/${archiveName} -C ${resultsBaseDir} .")
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300214 fileContents = salt.getFileContent(pepperEnv, minion, "/tmp/${archiveName}", true, null, false)
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300215
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300216 sh "mkdir -p ${localResultsDir}"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300217 writeFile file: "${archiveName}", text: fileContents
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300218 sh "tar --strip-components 1 -xf ${archiveName} --directory ${localResultsDir}; rm -f ${archiveName}"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300219
220 // Remove archive which is not needed anymore
221 salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', "/tmp/${archiveName}")
222
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000223 // Attempt to upload the scanning results to the dashboard
224 if (UPLOAD_TO_DASHBOARD.toBoolean()) {
225 if (common.validInputParam('DASHBOARD_API_URL')) {
226 def cloudName = salt.getGrain(pepperEnv, minion, 'domain')['return'][0].values()[0].values()[0]
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300227 def nodeResults = readFile "${localResultsDir}/${benchmarkPathWithoutExtension}/results.json"
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300228 try {
229 reportId = uploadResultToDashboard(DASHBOARD_API_URL, cloudName, minion, reportType, reportId, nodeResults)
230 } catch (Exception e) {
231 lastError = e
232 }
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000233 } else {
234 throw new Exception('Uploading to the dashboard is enabled but the DASHBOARD_API_URL was not set')
235 }
236 }
237 }
238 }
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300239
240 // Prepare archive
241 sh "tar -cJf ${artifactsDir}.tar.xz ${artifactsDir}"
242
243 // Archive the build output artifacts
244 archiveArtifacts artifacts: "*.xz"
Pavlo Shchelokovskyy011495f2018-10-10 13:03:57 +0300245 if (lastError) {
246 common.infoMsg('Uploading some results to the dashboard report ${reportId} failed. Raising last error.')
247 throw lastError
248 }
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000249 }
250
251/* // Will be implemented later
252 stage ('Attempt to upload results to an artifactory') {
253 if (common.validInputParam('ARTIFACTORY_URL')) {
254 for (minion in liveMinions) {
255 def destDir = "${artifactsDir}/${minion}"
256 def archiveName = "openscap-${scanUUID}.tar.gz"
257 def tempArchive = "/tmp/${archiveName}"
258 def destination = "${destDir}/${archiveName}"
259
260 dir(destDir) {
261 // Archive scanning results on the remote target
262 salt.runSaltProcessStep(pepperEnv, minion, 'archive.tar', ['czf', tempArchive, resultsBaseDir])
263
264 // Get it content and save it
265 writeFile file: destination, text: salt.getFileContent(pepperEnv, minion, tempArchive)
266
267 // Remove scanning results and the temp archive on the remote target
268 salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', resultsBaseDir)
269 salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', tempArchive)
270 }
271 }
272
273 def artifactory = new com.mirantis.mcp.MCPArtifactory()
274 def artifactoryName = 'mcp-ci'
275 def artifactoryRepo = ARTIFACTORY_REPO ?: 'binary-dev-local'
276 def artifactoryNamespace = ARTIFACTORY_NAMESPACE ?: 'mirantis/openscap'
277 def artifactoryServer = Artifactory.server(artifactoryName)
278 def publishInfo = true
279 def buildInfo = Artifactory.newBuildInfo()
280 def zipName = "${env.WORKSPACE}/openscap/${scanUUID}/results.zip"
281
282 // Zip scan results
283 zip zipFile: zipName, archive: false, dir: artifactsDir
284
285 // Mandatory and additional properties
286 def properties = artifactory.getBinaryBuildProperties([
287 "scanUuid=${scanUUID}",
288 "project=openscap"
289 ])
290
291 // Build Artifactory spec object
292 def uploadSpec = """{
293 "files":
294 [
295 {
296 "pattern": "${zipName}",
297 "target": "${artifactoryRepo}/${artifactoryNamespace}/openscap",
298 "props": "${properties}"
299 }
300 ]
301 }"""
302
303 // Upload artifacts to the given Artifactory
304 artifactory.uploadBinariesToArtifactory(artifactoryServer, buildInfo, uploadSpec, publishInfo)
305
306 } else {
307 common.warningMsg('ARTIFACTORY_URL was not given, skip uploading to artifactory')
308 }
309 }
310*/
311
312}