Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add createProject Task #11

Merged
merged 14 commits into from
Feb 1, 2024
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
72 changes: 72 additions & 0 deletions src/integrationTest/kotlin/com/liftric/dtcp/CreateProjectTest.kt
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
Original file line number Diff line number Diff line change
@@ -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

/**
Expand All @@ -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 =
Expand Down
12 changes: 12 additions & 0 deletions src/integrationTest/kotlin/com/liftric/dtcp/service/ApiService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
13 changes: 13 additions & 0 deletions src/main/kotlin/com/liftric/dtcp/DepTrackCompanionPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ class DepTrackCompanionPlugin : Plugin<Project> {
)
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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,9 +17,12 @@ abstract class DepTrackCompanionExtension(val project: Project) {
abstract val projectUUID: Property<String>
abstract val projectName: Property<String>
abstract val projectVersion: Property<String>
abstract val projectActive: Property<Boolean>
abstract val projectTags: ListProperty<ProjectTag>
abstract val parentUUID: Property<String>
abstract val parentName: Property<String>
abstract val parentVersion: Property<String>
abstract val ignoreProjectAlreadyExists: Property<Boolean>

abstract val riskScoreData: Property<RiskScoreBuilder>

Expand Down
23 changes: 21 additions & 2 deletions src/main/kotlin/com/liftric/dtcp/model/DependencyTrack.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ProjectTag>,
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,
Expand Down
12 changes: 12 additions & 0 deletions src/main/kotlin/com/liftric/dtcp/service/ApiService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -61,4 +62,15 @@ class ApiService(apiKey: String) {
}
}
}

suspend fun <T> putRequest(url: String, body: T, serializer: KSerializer<T>): HttpResponse {
val jsonBody = Json.encodeToString(serializer, body)
return client.put(url) {
headers {
append(HttpHeaders.ContentType, ContentType.Application.Json)
}
contentType(ContentType.Application.Json)
setBody(jsonBody)
}
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/com/liftric/dtcp/service/DependencyTrack.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
66 changes: 66 additions & 0 deletions src/main/kotlin/com/liftric/dtcp/tasks/CreateProject.kt
Original file line number Diff line number Diff line change
@@ -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<String>

@get:Input
abstract val url: Property<String>

@get:Input
abstract val projectName: Property<String>

@get:Input
@get:Optional
abstract val projectVersion: Property<String>

@get:Input
@get:Optional
abstract val projectActive: Property<Boolean>

@get:Input
@get:Optional
abstract val projectTags: ListProperty<ProjectTag>

@get:Input
@get:Optional
abstract val parentUUID: Property<String>

@get:Input
@get:Optional
abstract val ignoreProjectAlreadyExists: Property<Boolean>

@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
}
}
}
Loading