Skip to content

Commit 9748e0c

Browse files
committed
feat: Allow to archive vulnerability resolution definitions
Signed-off-by: Johanna Lamppu <[email protected]>
1 parent b91f97c commit 9748e0c

File tree

6 files changed

+369
-0
lines changed

6 files changed

+369
-0
lines changed

components/resolutions/backend/src/main/kotlin/VulnerabilityResolutionDefinitionService.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
package org.eclipse.apoapsis.ortserver.components.resolutions
2121

2222
import org.eclipse.apoapsis.ortserver.dao.dbQuery
23+
import org.eclipse.apoapsis.ortserver.dao.repositories.repositoryconfiguration.RepositoryConfigurationsTable
24+
import org.eclipse.apoapsis.ortserver.dao.repositories.repositoryconfiguration.RepositoryConfigurationsVulnerabilityResolutionsTable
2325
import org.eclipse.apoapsis.ortserver.dao.repositories.userDisplayName.UserDisplayNameDao
2426
import org.eclipse.apoapsis.ortserver.dao.tables.ChangeLogTable
2527
import org.eclipse.apoapsis.ortserver.dao.tables.VulnerabilityResolutionDefinitionsTable
@@ -29,6 +31,7 @@ import org.eclipse.apoapsis.ortserver.model.RepositoryId
2931
import org.eclipse.apoapsis.ortserver.model.UserDisplayName
3032
import org.eclipse.apoapsis.ortserver.model.VulnerabilityResolutionDefinition
3133
import org.eclipse.apoapsis.ortserver.model.VulnerabilityResolutionReason
34+
import org.eclipse.apoapsis.ortserver.model.util.asPresent
3235
import org.eclipse.apoapsis.ortserver.services.ortrun.OrtRunService
3336

3437
import org.jetbrains.exposed.sql.Database
@@ -67,4 +70,46 @@ class VulnerabilityResolutionDefinitionService(private val db: Database, private
6770

6871
VulnerabilityResolutionDefinitionsTable.get(id)
6972
}
73+
74+
suspend fun archive(id: Long, userDisplayName: UserDisplayName): VulnerabilityResolutionDefinition = db.dbQuery {
75+
val user =
76+
UserDisplayNameDao.insertOrUpdate(userDisplayName) ?: throw NullPointerException("No user created or found")
77+
78+
VulnerabilityResolutionDefinitionsTable.updateDefinition(
79+
id,
80+
archivedInput = true.asPresent()
81+
)
82+
83+
ChangeLogTable.insert(
84+
ChangeEventEntityType.VULNERABILITY_RESOLUTION_DEFINITION,
85+
id.toString(),
86+
user.id.value,
87+
ChangeEventAction.ARCHIVE
88+
)
89+
90+
ortRunService.markAsOutdated(
91+
getAffectedRuns(id),
92+
"Vulnerability resolution definition archived."
93+
)
94+
95+
VulnerabilityResolutionDefinitionsTable.get(id)
96+
}
97+
98+
suspend fun getById(id: Long): VulnerabilityResolutionDefinition? = db.dbQuery {
99+
VulnerabilityResolutionDefinitionsTable
100+
.getOrNull(id)
101+
}
102+
103+
private fun getAffectedRuns(
104+
definitionId: Long
105+
): List<Long> {
106+
return RepositoryConfigurationsVulnerabilityResolutionsTable
107+
.innerJoin(RepositoryConfigurationsTable)
108+
.select(RepositoryConfigurationsTable.ortRunId)
109+
.where {
110+
RepositoryConfigurationsVulnerabilityResolutionsTable.vulnerabilityResolutionDefinitionId eq
111+
definitionId
112+
}
113+
.map { it[RepositoryConfigurationsTable.ortRunId].value }
114+
}
70115
}

components/resolutions/backend/src/routes/kotlin/Routing.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package org.eclipse.apoapsis.ortserver.components.resolutions
2121

2222
import io.ktor.server.routing.Route
2323

24+
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.deleteVulnerabilityResolutionDefinition
2425
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.postVulnerabilityResolutionDefinition
2526
import org.eclipse.apoapsis.ortserver.services.ortrun.OrtRunService
2627

@@ -30,4 +31,5 @@ fun Route.resolutionsRoutes(
3031
vulnerabilityResolutionDefinitionService: VulnerabilityResolutionDefinitionService
3132
) {
3233
postVulnerabilityResolutionDefinition(ortRunService, vulnerabilityResolutionDefinitionService)
34+
deleteVulnerabilityResolutionDefinition(vulnerabilityResolutionDefinitionService)
3335
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities
21+
22+
import io.github.smiley4.ktoropenapi.delete
23+
24+
import io.ktor.http.HttpStatusCode
25+
import io.ktor.server.auth.principal
26+
import io.ktor.server.response.respond
27+
import io.ktor.server.routing.Route
28+
29+
import kotlinx.datetime.Instant
30+
31+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.AuthorizationException
32+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal
33+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getFullName
34+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId
35+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUsername
36+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission
37+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission
38+
import org.eclipse.apoapsis.ortserver.components.resolutions.VulnerabilityResolutionDefinitionService
39+
import org.eclipse.apoapsis.ortserver.model.UserDisplayName as ModelUserDisplayName
40+
import org.eclipse.apoapsis.ortserver.shared.apimappings.mapToApi
41+
import org.eclipse.apoapsis.ortserver.shared.apimodel.ChangeEvent
42+
import org.eclipse.apoapsis.ortserver.shared.apimodel.ChangeEventAction
43+
import org.eclipse.apoapsis.ortserver.shared.apimodel.UserDisplayName
44+
import org.eclipse.apoapsis.ortserver.shared.apimodel.VulnerabilityResolutionDefinition
45+
import org.eclipse.apoapsis.ortserver.shared.apimodel.VulnerabilityResolutionReason
46+
import org.eclipse.apoapsis.ortserver.shared.ktorutils.jsonBody
47+
import org.eclipse.apoapsis.ortserver.shared.ktorutils.requireIdParameter
48+
import org.eclipse.apoapsis.ortserver.shared.ktorutils.respondError
49+
50+
internal fun Route.deleteVulnerabilityResolutionDefinition(
51+
vulnerabilityResolutionDefinitionService: VulnerabilityResolutionDefinitionService
52+
) = delete("/resolutions/vulnerabilities/{id}", {
53+
operationId = "DeleteVulnerabilityResolutionDefinition"
54+
summary = "Delete a vulnerability resolution"
55+
tags = listOf("Resolutions")
56+
57+
request {
58+
pathParameter<Long>("id") {
59+
description = "The ID of the vulnerability resolution definition"
60+
}
61+
}
62+
63+
response {
64+
HttpStatusCode.OK to {
65+
description = "Success"
66+
67+
jsonBody<VulnerabilityResolutionDefinition> {
68+
example("Delete Vulnerability Resolution Definition") {
69+
value = VulnerabilityResolutionDefinition(
70+
id = 1,
71+
idMatchers = listOf("CVE-2020-15250", "GHSA-269g-pwp5-87pp"),
72+
reason = VulnerabilityResolutionReason.WILL_NOT_FIX_VULNERABILITY,
73+
comment = "Comment",
74+
archived = true,
75+
changes = listOf(
76+
ChangeEvent(
77+
user = UserDisplayName(username = "User"),
78+
occurredAt = Instant.parse("2024-01-01T00:00:00Z"),
79+
ChangeEventAction.CREATE
80+
),
81+
ChangeEvent(
82+
user = UserDisplayName(username = "User"),
83+
occurredAt = Instant.parse("2024-01-02T00:00:00Z"),
84+
ChangeEventAction.ARCHIVE
85+
)
86+
)
87+
)
88+
}
89+
}
90+
}
91+
92+
HttpStatusCode.NoContent to {
93+
description = "The vulnerability resolution definition was already archived."
94+
}
95+
}
96+
}) {
97+
val id = call.requireIdParameter("id")
98+
99+
val definition = vulnerabilityResolutionDefinitionService.getById(id)
100+
101+
if (definition == null) throw AuthorizationException()
102+
103+
requirePermission(RepositoryPermission.WRITE.roleName(definition.hierarchyId.value))
104+
105+
if (definition.archived) {
106+
call.respond(HttpStatusCode.NoContent)
107+
return@delete
108+
}
109+
110+
// Extract the user information from the principal.
111+
val userDisplayName = call.principal<OrtPrincipal>()?.let { principal ->
112+
ModelUserDisplayName(principal.getUserId(), principal.getUsername(), principal.getFullName())
113+
}
114+
115+
if (userDisplayName == null) {
116+
call.respondError(HttpStatusCode.InternalServerError, "Unable to resolve user display name from token.")
117+
return@delete
118+
}
119+
120+
val archivedDefinition = vulnerabilityResolutionDefinitionService.archive(id, userDisplayName).mapToApi()
121+
122+
call.respond(HttpStatusCode.OK, archivedDefinition)
123+
}

components/resolutions/backend/src/test/kotlin/VulnerabilityResolutionDefinitionServiceTest.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,65 @@ class VulnerabilityResolutionDefinitionServiceTest : WordSpec({
132132
}
133133
}
134134
}
135+
136+
"archive" should {
137+
"archive a vulnerability resolution definition" {
138+
val userDisplayName = UserDisplayName(
139+
"abc",
140+
"Test",
141+
"Test User"
142+
)
143+
144+
val definitionId = definitionService.create(
145+
RepositoryId(repositoryId),
146+
runId,
147+
userDisplayName,
148+
listOf("CVE-2020-15250"),
149+
VulnerabilityResolutionReason.INEFFECTIVE_VULNERABILITY,
150+
"Comment."
151+
).id
152+
153+
val archivedDefinition = definitionService.archive(definitionId, userDisplayName)
154+
155+
with(archivedDefinition) {
156+
archived shouldBe true
157+
changes shouldHaveSize 2
158+
159+
with(changes.first()) {
160+
user shouldBe userDisplayName
161+
action shouldBe ChangeEventAction.CREATE
162+
}
163+
164+
with(changes.last()) {
165+
user shouldBe userDisplayName
166+
action shouldBe ChangeEventAction.ARCHIVE
167+
}
168+
}
169+
}
170+
}
171+
172+
"getById" should {
173+
"return the vulnerability resolution definition if it exists" {
174+
val userDisplayName = UserDisplayName(
175+
"abc",
176+
"Test",
177+
"Test User"
178+
)
179+
180+
val definition = definitionService.create(
181+
RepositoryId(repositoryId),
182+
runId,
183+
userDisplayName,
184+
listOf("CVE-2020-15250"),
185+
VulnerabilityResolutionReason.INEFFECTIVE_VULNERABILITY,
186+
"Comment."
187+
)
188+
189+
definitionService.getById(definition.id) shouldBe definition
190+
}
191+
192+
"return null if the vulnerability resolution definition doesn't exist" {
193+
definitionService.getById(1) shouldBe null
194+
}
195+
}
135196
})

components/resolutions/backend/src/test/kotlin/routes/ResolutionsAuthorizationTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.eclipse.apoapsis.ortserver.components.resolutions.routes
2121

22+
import io.ktor.client.request.delete
2223
import io.ktor.client.request.post
2324
import io.ktor.client.request.setBody
2425
import io.ktor.http.HttpStatusCode
@@ -30,7 +31,10 @@ import org.eclipse.apoapsis.ortserver.components.resolutions.CreateVulnerability
3031
import org.eclipse.apoapsis.ortserver.components.resolutions.VulnerabilityResolutionDefinitionService
3132
import org.eclipse.apoapsis.ortserver.components.resolutions.resolutionsRoutes
3233
import org.eclipse.apoapsis.ortserver.dao.test.Fixtures
34+
import org.eclipse.apoapsis.ortserver.model.RepositoryId
35+
import org.eclipse.apoapsis.ortserver.model.UserDisplayName
3336
import org.eclipse.apoapsis.ortserver.services.ortrun.OrtRunService
37+
import org.eclipse.apoapsis.ortserver.shared.apimappings.mapToModel
3438
import org.eclipse.apoapsis.ortserver.shared.apimodel.VulnerabilityResolutionReason
3539
import org.eclipse.apoapsis.ortserver.shared.ktorutils.AbstractAuthorizationTest
3640

@@ -115,4 +119,33 @@ class ResolutionsAuthorizationTest : AbstractAuthorizationTest({
115119
}
116120
}
117121
}
122+
123+
"DeleteVulnerabilityResolutionDefinition" should {
124+
"require role RepositoryPermission.WRITE.roleName(repositoryId)" {
125+
val definitionId = definitionService.create(
126+
RepositoryId(repositoryId),
127+
runId,
128+
UserDisplayName("abc", "Test"),
129+
createBody.idMatchers,
130+
createBody.reason.mapToModel(),
131+
createBody.comment
132+
).id
133+
134+
requestShouldRequireRole(
135+
routes = { resolutionsRoutes(ortRunService, definitionService) },
136+
role = RepositoryPermission.WRITE.roleName(repositoryId)
137+
) {
138+
delete("/resolutions/vulnerabilities/$definitionId")
139+
}
140+
}
141+
142+
"respond with 'Forbidden' when repository id cannot be resolved" {
143+
requestShouldRequireAuthentication(
144+
routes = { resolutionsRoutes(ortRunService, definitionService) },
145+
successStatus = HttpStatusCode.Forbidden
146+
) {
147+
delete("/resolutions/vulnerabilities/9999")
148+
}
149+
}
150+
}
118151
})

0 commit comments

Comments
 (0)