From 6cf6ca6b3517d8325f36120026de49d9da0f6cf5 Mon Sep 17 00:00:00 2001 From: "~ . ~" Date: Tue, 8 Oct 2024 13:33:56 -0400 Subject: [PATCH] support file:// --- .../oscal/tools/server/OscalVerticle.kt | 76 ++++++++---- .../oscal/tools/server/TestOscalVerticle.kt | 113 ++++++++++++++++-- 2 files changed, 155 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/gov/nist/secauto/oscal/tools/server/OscalVerticle.kt b/src/main/kotlin/gov/nist/secauto/oscal/tools/server/OscalVerticle.kt index 364aeb2..7516381 100644 --- a/src/main/kotlin/gov/nist/secauto/oscal/tools/server/OscalVerticle.kt +++ b/src/main/kotlin/gov/nist/secauto/oscal/tools/server/OscalVerticle.kt @@ -77,6 +77,33 @@ class OscalVerticle : CoroutineVerticle() { router.route("/*").handler(StaticHandler.create("webroot")) return router } + private fun processUrl(url: String): String { + return if (url.startsWith("file://")) { + try { + val uri = URI(url) + val path = when { + uri.authority != null -> { + // Remove the authority component + Paths.get(uri.authority + uri.path) + } + uri.path.startsWith("/") -> { + // Absolute path + Paths.get(uri.path) + } + else -> { + // Relative path + Paths.get(uri.path).toAbsolutePath() + } + } + path.toString() + } catch (e: Exception) { + logger.error("Error processing file URL: $url", e) + url // Return original URL if processing fails + } + } else { + url + } + } private fun handleValidateFileUpload(ctx: RoutingContext) { logger.info("Handling file upload request!") launch { @@ -98,11 +125,15 @@ class OscalVerticle : CoroutineVerticle() { // Use async for parallelism val result = async { - executeCommand(listOf("validate", tempFilePath.toString())) + executeCommand(listOf("validate", tempFilePath.toString(),"--show-stack-trace")) }.await() // Wait for the result of the async execution logger.info("Validation result: ${result.second}") - sendSuccessResponse(ctx, result.first, result.second) + if(result.first.exitCode.toString()==="OK"){ + sendSuccessResponse(ctx, result.first, result.second) + }else{ + sendErrorResponse(ctx, 400, result.first.exitCode.toString()) + } // Clean up the temporary file } else { @@ -131,11 +162,16 @@ class OscalVerticle : CoroutineVerticle() { try { logger.info("Handling Validate request") val encodedContent = ctx.queryParam("content").firstOrNull() - val content = encodedContent?.let { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) } - if (content != null) { + if (encodedContent != null) { + val content = processUrl(encodedContent) // Use async for parallelism val result = async { - executeCommand(parseCommandToArgs("validate "+content)) + try{ + executeCommand(listOf("validate",content,"--show-stack-trace")) + }catch (e: Exception) { + logger.error("Error handling request", e) + executeCommand(listOf("validate",content,"--show-stack-trace")) + } }.await() // Wait for the result of the async execution logger.info(result.second) sendSuccessResponse(ctx, result.first, result.second) @@ -143,7 +179,7 @@ class OscalVerticle : CoroutineVerticle() { sendErrorResponse(ctx, 400, "content parameter is missing") } } catch (e: Exception) { - logger.error("Error handling CLI request", e) + logger.error("Error handling request", e) sendErrorResponse(ctx, 500, "Internal server error") } } @@ -151,13 +187,11 @@ class OscalVerticle : CoroutineVerticle() { private fun handleResolveRequest(ctx: RoutingContext) { launch { try { - logger.info("Handling Resolve request") - val encodedContent = ctx.queryParam("content").firstOrNull() - val content = encodedContent?.let { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) } - val acceptHeader = ctx.request().getHeader("Accept") - val format = mapMimeTypeToFormat(acceptHeader) - - if (content != null) { + val encodedContent = ctx.queryParam("content").firstOrNull() + if (encodedContent != null) { + val content = processUrl(encodedContent) + val acceptHeader = ctx.request().getHeader("Accept") + val format = mapMimeTypeToFormat(acceptHeader) // Use async for parallelism val result = async { executeCommand(parseCommandToArgs("resolve-profile $content --to=$format")) @@ -184,14 +218,11 @@ class OscalVerticle : CoroutineVerticle() { private fun handleConvertRequest(ctx: RoutingContext) { launch { try { - logger.info("Handling Convert request") - val encodedContent = ctx.queryParam("content").firstOrNull() - val content = encodedContent?.let { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) } - val acceptHeader = ctx.request().getHeader("Accept") - val format = mapMimeTypeToFormat(acceptHeader) - - if (content != null) { - // Use async for parallelism + val encodedContent = ctx.queryParam("content").firstOrNull() + if (encodedContent != null) { + val content = processUrl(encodedContent) + val acceptHeader = ctx.request().getHeader("Accept") + val format = mapMimeTypeToFormat(acceptHeader) val result = async { executeCommand(parseCommandToArgs("convert $content --to=$format")) }.await() // Wait for the result of the async execution @@ -281,13 +312,14 @@ class OscalVerticle : CoroutineVerticle() { ctx.response() .setStatusCode(200) // HTTP 200 OK .putHeader("Content-Type", "application/json") - .putHeader("Exit-Status", exitStatus.toString()) + .putHeader("Exit-Status", exitStatus.exitCode.toString()) .end(fileContent) } private fun sendErrorResponse(ctx: RoutingContext, statusCode: Int, message: String) { ctx.response() .setStatusCode(statusCode) + .putHeader("Exit-Status", statusCode.toString()) .putHeader("content-type", "application/json") .end(JsonObject().put("error", message).encode()) } diff --git a/src/test/kotlin/gov/nist/secauto/oscal/tools/server/TestOscalVerticle.kt b/src/test/kotlin/gov/nist/secauto/oscal/tools/server/TestOscalVerticle.kt index 9c84cb2..90c25be 100644 --- a/src/test/kotlin/gov/nist/secauto/oscal/tools/server/TestOscalVerticle.kt +++ b/src/test/kotlin/gov/nist/secauto/oscal/tools/server/TestOscalVerticle.kt @@ -1,10 +1,5 @@ -/* - * SPDX-FileCopyrightText: none - * SPDX-License-Identifier: CC0-1.0 - */ - package gov.nist.secauto.oscal.tools.server - +import java.nio.file.Files import io.vertx.core.Vertx import io.vertx.ext.web.client.WebClient import io.vertx.ext.web.client.WebClientOptions @@ -14,11 +9,19 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith - +import java.nio.file.Path +import java.nio.file.Paths +import java.io.File +import java.net.URL +import java.net.URLEncoder +import java.net.URI +import kotlinx.coroutines.runBlocking +import org.apache.logging.log4j.Logger +import org.apache.logging.log4j.LogManager @RunWith(VertxUnitRunner::class) class TestOscalVerticle { - + private val logger: Logger = LogManager.getLogger(TestOscalVerticle::class.java) private lateinit var vertx: Vertx private lateinit var webClient: WebClient @@ -26,7 +29,8 @@ class TestOscalVerticle { fun setUp(testContext: TestContext) { vertx = Vertx.vertx() webClient = WebClient.create(vertx, WebClientOptions().setDefaultPort(8888)) - + initializeOscalDirectory() + val async = testContext.async() vertx.deployVerticle(OscalVerticle()) { ar -> if (ar.succeeded()) { @@ -43,11 +47,11 @@ class TestOscalVerticle { } @Test - fun test_oscal_command(testContext: TestContext) { + fun test_oscal_command_remote(testContext: TestContext) { val async = testContext.async() webClient.get("/validate") - .addQueryParam("content", "https://raw.githubusercontent.com/GSA/fedramp-automation/refs/heads/develop/src/validations/constraints/content/ssp-all-VALID.xml") + .addQueryParam("content", "https://raw.githubusercontent.com/usnistgov/oscal-content/refs/heads/main/examples/ssp/xml/ssp-example.xml") .send { ar -> if (ar.succeeded()) { val response = ar.result() @@ -55,11 +59,96 @@ class TestOscalVerticle { val body = response.bodyAsJsonObject() testContext.assertNotNull(body) testContext.assertTrue(body.containsKey("runs")) - // New assertion to check output length async.complete() } else { testContext.fail(ar.cause()) } } } + private fun initializeOscalDirectory() { + val homeDir = System.getProperty("user.home") + val oscalDir = Paths.get(homeDir, ".oscal") + val oscalTmpDir = oscalDir.resolve("tmp") + if (!Files.exists(oscalDir)) { + Files.createDirectory(oscalDir) + } + if (!Files.exists(oscalTmpDir)) { + Files.createDirectory(oscalTmpDir) + } + } + + @Test + fun test_oscal_command_local_file(testContext: TestContext) { + val async = testContext.async() + + try { + // Download the file + val url = URL("https://raw.githubusercontent.com/usnistgov/oscal-content/refs/heads/main/examples/ssp/xml/ssp-example.xml") + val homeDir = System.getProperty("user.home") + val oscalDir = Paths.get(homeDir, ".oscal") + + // Ensure the OSCAL directory exists + if (!Files.exists(oscalDir)) { + Files.createDirectories(oscalDir) + logger.info("Created OSCAL directory: $oscalDir") + } + + var tempFile: Path? = null + try { + tempFile = Files.createTempFile(oscalDir, "ssp", ".xml") + logger.info("Created temporary file: $tempFile") + } catch (e: Exception) { + logger.error("Failed to create temporary file in $oscalDir", e) + testContext.fail("Failed to create temporary file: ${e.message}") + return@test_oscal_command_local_file + } + + val tempFilePath = tempFile.toAbsolutePath() + + runBlocking { + try { + url.openStream().use { input -> + Files.newOutputStream(tempFile).use { output -> + input.copyTo(output) + } + } + logger.info("Successfully downloaded content to $tempFile") + } catch (e: Exception) { + logger.error("Failed to download or write content", e) + testContext.fail("Failed to download or write content: ${e.message}") + return@runBlocking + } + } + + val fileUrl = "file://" + URLEncoder.encode(tempFilePath.toString(), "UTF-8") + + webClient.get("/validate") + .addQueryParam("content", fileUrl) + .send { ar -> + if (ar.succeeded()) { + val response = ar.result() + testContext.assertEquals(200, response.statusCode()) + val body = response.bodyAsJsonObject() + testContext.assertEquals("OK", response.getHeader("Exit-Status")) + testContext.assertNotNull(body) + testContext.assertTrue(body.containsKey("runs")) + async.complete() + } else { + logger.error("Validation request failed", ar.cause()) + testContext.fail(ar.cause()) + } + + // Clean up the temporary file + try { + Files.deleteIfExists(tempFile) + logger.info("Deleted temporary file: $tempFile") + } catch (e: Exception) { + logger.warn("Failed to delete temporary file: $tempFile", e) + } + } + } catch (e: Exception) { + logger.error("Unexpected error in test", e) + testContext.fail(e) + } + } } \ No newline at end of file