diff --git a/README.md b/README.md index 0584700..88cbf29 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This plugin internally applies the [CycloneDX Gradle plugin](https://github.com/ The plugin offers several tasks: - `runDepTrackWorkflow`: Runs `generateSbom`, `uploadSbom`, `generateVex`, `uploadVex` and `riskScore` tasks for CI/CD. +- `createProject`: Creates a Project - `generateSbom`: Generates the SBOM (Runs "cyclonedxBom" from [cyclonedx-gradle-plugin](https://github.com/CycloneDX/cyclonedx-gradle-plugin) under the hood) - `uploadSbom`: Uploads SBOM file. - `generateVex`: Generates VEX file. @@ -20,6 +21,17 @@ The plugin offers several tasks: Each task requires certain inputs which are to be specified in your `build.gradle.kts`. The configuration for each task is as follows: +#### createProject + +- `url`: Dependency Track API URL +- `apiKey`: Dependency Track API KEY +- `projectName`: The Name of the Project you want to create +- `projectVersion`: *Optional* - The Version of the Project you want to create +- `projectActive`: *Optional* - default is true, set to false to create an inactive Project +- `projectTags`: *Optional* - add Tags to your Project +- `parentUUID`: *Optional* - Used for creating in a parent project +- `ignoreProjectAlreadyExists`: *Optional* - default is false, set to true to ignore "Project already exist" error + #### uploadSbom - `url`: Dependency Track API URL diff --git a/src/integrationTest/kotlin/com/liftric/dtcp/CreateProjectTest.kt b/src/integrationTest/kotlin/com/liftric/dtcp/CreateProjectTest.kt new file mode 100644 index 0000000..2d0211b --- /dev/null +++ b/src/integrationTest/kotlin/com/liftric/dtcp/CreateProjectTest.kt @@ -0,0 +1,72 @@ +package com.liftric.dtcp + +import com.liftric.dtcp.service.ApiService +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.io.File +import kotlinx.coroutines.runBlocking +import org.gradle.testkit.runner.UnexpectedBuildFailure + +class CreateProjectTest : IntegrationTestBase() { + @Test + fun testCreateProjectTest() { + val projectName = "createProjectTest" + val version = "1.0.0" + + val apiService = ApiService(dependencyTrackApiEndpoint) + + val dependencyTrackAccessKey = + runBlocking { apiService.getDependencyTrackAccessKey() } + + assertTrue(dependencyTrackAccessKey.isNotEmpty()) + + val projectDir = File("build/createProjectTest") + + projectDir.mkdirs() + projectDir.resolve("settings.gradle.kts").writeText("") + projectDir.resolve("build.gradle.kts").writeText( + """ +import com.liftric.dtcp.extensions.* + +plugins { + kotlin("jvm") version "1.8.21" + id("com.liftric.dependency-track-companion-plugin") +} + +repositories { + mavenCentral() +} + +group = "com.liftric.$projectName" +version = "$version" + +dependencyTrackCompanion { + url.set("$dependencyTrackApiEndpoint") + apiKey.set("$dependencyTrackAccessKey") + projectName.set("$projectName") + projectVersion.set("$version") + projectActive.set(false) +} + """ + ) + + /** + * GradleRunner fails under the hood, but the Project is created successfully. + * see [IgnoreErrorApiService] for more info. + * */ + try { + GradleRunner + .create() + .withProjectDir(projectDir) + .withArguments("build", "createProject") + .withPluginClasspath().build() + } catch (e: UnexpectedBuildFailure) { + assertTrue(e.message!!.contains("/api/v1/project: 500 Server Error")) + } + + runBlocking { + assertTrue(apiService.verifyProjectCreation(dependencyTrackAccessKey, projectName, version)) + } + } +} diff --git a/src/integrationTest/kotlin/com/liftric/dtcp/RunDepTrackWorkflowTest.kt b/src/integrationTest/kotlin/com/liftric/dtcp/RunDepTrackWorkflowTest.kt index de5b176..d91baea 100644 --- a/src/integrationTest/kotlin/com/liftric/dtcp/RunDepTrackWorkflowTest.kt +++ b/src/integrationTest/kotlin/com/liftric/dtcp/RunDepTrackWorkflowTest.kt @@ -1,16 +1,11 @@ package com.liftric.dtcp -import com.liftric.dtcp.model.VexComponent -import com.liftric.dtcp.model.VexVulnerability import com.liftric.dtcp.service.ApiService import com.liftric.dtcp.service.IgnoreErrorApiService import org.gradle.testkit.runner.GradleRunner import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.io.File -import java.nio.file.Files -import java.nio.file.Paths -import java.nio.file.StandardCopyOption import kotlinx.coroutines.runBlocking /** @@ -25,7 +20,7 @@ import kotlinx.coroutines.runBlocking class RunDepTrackWorkflowTest: IntegrationTestBase() { @Test fun testRunDepTrackWorkflowTest() { - val projectName = "dtTest" + val projectName = "runDepTrackWorkflowTest" val version = "1.0.0" val dependencyTrackAccessKey = diff --git a/src/integrationTest/kotlin/com/liftric/dtcp/service/ApiService.kt b/src/integrationTest/kotlin/com/liftric/dtcp/service/ApiService.kt index e936fc5..7f2c35a 100644 --- a/src/integrationTest/kotlin/com/liftric/dtcp/service/ApiService.kt +++ b/src/integrationTest/kotlin/com/liftric/dtcp/service/ApiService.kt @@ -7,6 +7,7 @@ import io.ktor.client.call.* import io.ktor.client.engine.cio.* import io.ktor.client.request.* import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.statement.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json @@ -31,4 +32,15 @@ class ApiService(private val dependencyTrackApiEndpoint: String) { val response: KeyResponse = client.put("$dependencyTrackApiEndpoint/api/v1/team/$adminUuid/key").body() return response.key } + + suspend fun verifyProjectCreation(dependencyTrackAccessKey: String, name: String, version: String): Boolean { + val response: HttpResponse = + client.get("${dependencyTrackApiEndpoint}/api/v1/project/lookup?name=$name&version=$version") { + headers { + append("X-Api-Key", dependencyTrackAccessKey) + append("Content-Type", "application/json") + } + } + return response.status.value == 200 + } } diff --git a/src/integrationTest/kotlin/com/liftric/dtcp/service/IgnoreErrorApiService.kt b/src/integrationTest/kotlin/com/liftric/dtcp/service/IgnoreErrorApiService.kt index 5a43b19..fb45a5e 100644 --- a/src/integrationTest/kotlin/com/liftric/dtcp/service/IgnoreErrorApiService.kt +++ b/src/integrationTest/kotlin/com/liftric/dtcp/service/IgnoreErrorApiService.kt @@ -42,7 +42,7 @@ class IgnoreErrorApiService( client.put("${dependencyTrackApiEndpoint}/api/v1/project") { headers { - append("X-Api-Key", "$dependencyTrackAccessKey") + append("X-Api-Key", dependencyTrackAccessKey) append("Content-Type", "application/json") } setBody(Json.encodeToString(projectData)) diff --git a/src/main/kotlin/com/liftric/dtcp/DepTrackCompanionPlugin.kt b/src/main/kotlin/com/liftric/dtcp/DepTrackCompanionPlugin.kt index cf248a5..0602378 100644 --- a/src/main/kotlin/com/liftric/dtcp/DepTrackCompanionPlugin.kt +++ b/src/main/kotlin/com/liftric/dtcp/DepTrackCompanionPlugin.kt @@ -22,6 +22,19 @@ class DepTrackCompanionPlugin : Plugin { ) extension.autoCreate.convention(false) + val createProject = project.tasks.register("createProject", CreateProject::class.java) { task -> + task.group = taskGroup + task.description = "Creates a project" + task.url.set(extension.url) + task.apiKey.set(extension.apiKey) + task.projectActive.set(extension.projectActive) + task.projectTags.set(extension.projectTags) + task.projectName.set(extension.projectName) + task.projectVersion.set(extension.projectVersion) + task.parentUUID.set(extension.parentUUID) + task.ignoreProjectAlreadyExists.set(extension.ignoreProjectAlreadyExists) + } + val generateSbom = project.tasks.register("generateSbom") { task -> task.group = taskGroup task.description = "Generate SBOM file" diff --git a/src/main/kotlin/com/liftric/dtcp/extensions/DepTrackCompanionExtension.kt b/src/main/kotlin/com/liftric/dtcp/extensions/DepTrackCompanionExtension.kt index d58cb26..298c957 100644 --- a/src/main/kotlin/com/liftric/dtcp/extensions/DepTrackCompanionExtension.kt +++ b/src/main/kotlin/com/liftric/dtcp/extensions/DepTrackCompanionExtension.kt @@ -1,5 +1,6 @@ package com.liftric.dtcp.extensions +import com.liftric.dtcp.model.ProjectTag import org.gradle.api.Project import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.ListProperty @@ -16,9 +17,12 @@ abstract class DepTrackCompanionExtension(val project: Project) { abstract val projectUUID: Property abstract val projectName: Property abstract val projectVersion: Property + abstract val projectActive: Property + abstract val projectTags: ListProperty abstract val parentUUID: Property abstract val parentName: Property abstract val parentVersion: Property + abstract val ignoreProjectAlreadyExists: Property abstract val riskScoreData: Property diff --git a/src/main/kotlin/com/liftric/dtcp/model/DependencyTrack.kt b/src/main/kotlin/com/liftric/dtcp/model/DependencyTrack.kt index cfde742..9071b5e 100644 --- a/src/main/kotlin/com/liftric/dtcp/model/DependencyTrack.kt +++ b/src/main/kotlin/com/liftric/dtcp/model/DependencyTrack.kt @@ -9,12 +9,12 @@ data class Component( val version: String, val purl: String, val uuid: String, - val repositoryMeta: RepositoryMeta? = null + val repositoryMeta: RepositoryMeta? = null, ) @Serializable data class RepositoryMeta( - val latestVersion: String + val latestVersion: String, ) @Serializable @@ -28,6 +28,25 @@ data class Project( val lastInheritedRiskScore: Double? = null, ) +@Serializable +data class CreateProject( + val name: String, + val version: String? = null, + val active: Boolean, + val tags: List, + val parent: Parent? = null, +) { + @Serializable + data class Parent( + val uuid: String? = null, + ) +} + +@Serializable +data class ProjectTag( + val name: String, +) + @Serializable data class DirectDependency( val name: String, diff --git a/src/main/kotlin/com/liftric/dtcp/service/ApiService.kt b/src/main/kotlin/com/liftric/dtcp/service/ApiService.kt index e234c13..0860c45 100644 --- a/src/main/kotlin/com/liftric/dtcp/service/ApiService.kt +++ b/src/main/kotlin/com/liftric/dtcp/service/ApiService.kt @@ -9,6 +9,7 @@ import io.ktor.client.statement.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* import io.ktor.http.* +import kotlinx.serialization.KSerializer import kotlinx.serialization.json.Json import java.io.File @@ -61,4 +62,15 @@ class ApiService(apiKey: String) { } } } + + suspend fun putRequest(url: String, body: T, serializer: KSerializer): HttpResponse { + val jsonBody = Json.encodeToString(serializer, body) + return client.put(url) { + headers { + append(HttpHeaders.ContentType, ContentType.Application.Json) + } + contentType(ContentType.Application.Json) + setBody(jsonBody) + } + } } diff --git a/src/main/kotlin/com/liftric/dtcp/service/DependencyTrack.kt b/src/main/kotlin/com/liftric/dtcp/service/DependencyTrack.kt index cc151e5..0c84a98 100644 --- a/src/main/kotlin/com/liftric/dtcp/service/DependencyTrack.kt +++ b/src/main/kotlin/com/liftric/dtcp/service/DependencyTrack.kt @@ -101,4 +101,9 @@ class DependencyTrack(apiKey: String, private val baseUrl: String) { } while (response.processing) println("Analysis is complete.") } + + fun createProject(project: CreateProject) = runBlocking { + val url = "$baseUrl/api/v1/project" + client.putRequest(url, project, CreateProject.serializer()) + } } diff --git a/src/main/kotlin/com/liftric/dtcp/tasks/CreateProject.kt b/src/main/kotlin/com/liftric/dtcp/tasks/CreateProject.kt new file mode 100644 index 0000000..6540524 --- /dev/null +++ b/src/main/kotlin/com/liftric/dtcp/tasks/CreateProject.kt @@ -0,0 +1,66 @@ +package com.liftric.dtcp.tasks + +import com.liftric.dtcp.model.CreateProject +import com.liftric.dtcp.model.ProjectTag +import com.liftric.dtcp.service.DependencyTrack +import org.gradle.api.DefaultTask +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional + +abstract class CreateProject : DefaultTask() { + @get:Input + abstract val apiKey: Property + + @get:Input + abstract val url: Property + + @get:Input + abstract val projectName: Property + + @get:Input + @get:Optional + abstract val projectVersion: Property + + @get:Input + @get:Optional + abstract val projectActive: Property + + @get:Input + @get:Optional + abstract val projectTags: ListProperty + + @get:Input + @get:Optional + abstract val parentUUID: Property + + @get:Input + @get:Optional + abstract val ignoreProjectAlreadyExists: Property + + @TaskAction + fun createProjectTask() { + val dt = DependencyTrack(apiKey.get(), url.get()) + + val project = CreateProject( + name = projectName.get(), + version = projectVersion.orNull, + active = projectActive.orNull ?: true, + tags = projectTags.getOrElse(emptyList()), + parent = parentUUID.orNull?.let { CreateProject.Parent(it) } + ) + + try { + dt.createProject(project) + } catch (e: Exception) { + if (ignoreProjectAlreadyExists.getOrElse(false) && e.message?.contains("already exists") == true) { + logger.info("Project already exists, ignoring") + return + } + logger.error("Error creating project: ${e.message}") + throw e + } + } +}