Skip to content

Commit c255005

Browse files
committed
feat: Allow to archive vulnerability resolutions
Implement deleting a vulnerability resolution by archiving the item, i.e. with soft deletion. The resolution will be marked as archived, which means that on new runs, the resolution won't be injected in the repository configuration anymore. The resolution will still be a part of the results of any run where it was injected earlier, and the information that it originated from a definition on the server is retained. Relates to #1009. Signed-off-by: Johanna Lamppu <[email protected]>
1 parent c6e249b commit c255005

File tree

6 files changed

+403
-0
lines changed

6 files changed

+403
-0
lines changed

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

Lines changed: 39 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
@@ -60,6 +63,29 @@ class VulnerabilityResolutionDefinitionService(private val db: Database, private
6063
VulnerabilityResolutionDefinitionsTable.get(id)
6164
}
6265

66+
suspend fun archive(id: Long, userDisplayName: UserDisplayName): VulnerabilityResolutionDefinition = db.dbQuery {
67+
VulnerabilityResolutionDefinitionsTable.updateDefinition(
68+
id,
69+
archivedInput = true.asPresent()
70+
)
71+
72+
addChangeLogEvent(id, ChangeEventAction.ARCHIVE, userDisplayName)
73+
74+
val definition = VulnerabilityResolutionDefinitionsTable.get(id)
75+
76+
ortRunService.markAsOutdated(
77+
getAffectedRuns(id) + definition.contextRunId,
78+
"Vulnerability resolution definition archived."
79+
)
80+
81+
definition
82+
}
83+
84+
suspend fun getById(id: Long): VulnerabilityResolutionDefinition? = db.dbQuery {
85+
VulnerabilityResolutionDefinitionsTable
86+
.getOrNull(id)
87+
}
88+
6389
private fun addChangeLogEvent(
6490
entityId: Long,
6591
action: ChangeEventAction,
@@ -75,4 +101,17 @@ class VulnerabilityResolutionDefinitionService(private val db: Database, private
75101
action
76102
)
77103
}
104+
105+
private fun getAffectedRuns(
106+
definitionId: Long
107+
): List<Long> {
108+
return RepositoryConfigurationsVulnerabilityResolutionsTable
109+
.innerJoin(RepositoryConfigurationsTable)
110+
.select(RepositoryConfigurationsTable.ortRunId)
111+
.where {
112+
RepositoryConfigurationsVulnerabilityResolutionsTable.vulnerabilityResolutionDefinitionId eq
113+
definitionId
114+
}
115+
.map { it[RepositoryConfigurationsTable.ortRunId].value }
116+
}
78117
}

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.deleteVulnerabilityResolution
2425
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.postVulnerabilityResolution
2526
import org.eclipse.apoapsis.ortserver.services.ortrun.OrtRunService
2627

@@ -30,4 +31,5 @@ fun Route.resolutionsRoutes(
3031
vulnerabilityResolutionDefinitionService: VulnerabilityResolutionDefinitionService
3132
) {
3233
postVulnerabilityResolution(ortRunService, vulnerabilityResolutionDefinitionService)
34+
deleteVulnerabilityResolution(vulnerabilityResolutionDefinitionService)
3335
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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.deleteVulnerabilityResolution(
51+
vulnerabilityResolutionDefinitionService: VulnerabilityResolutionDefinitionService
52+
) = delete("/resolutions/vulnerabilities/{id}", {
53+
operationId = "deleteVulnerabilityResolution"
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") {
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 was already archived."
94+
}
95+
}
96+
}) {
97+
val id = call.requireIdParameter("id")
98+
99+
val definition = vulnerabilityResolutionDefinitionService.getById(id) ?: throw AuthorizationException()
100+
101+
requirePermission(RepositoryPermission.WRITE.roleName(definition.hierarchyId.value))
102+
103+
if (definition.archived) {
104+
call.respond(HttpStatusCode.NoContent, "The vulnerability resolution was already archived.")
105+
return@delete
106+
}
107+
108+
// Extract the user information from the principal.
109+
val userDisplayName = call.principal<OrtPrincipal>()?.let { principal ->
110+
ModelUserDisplayName(principal.getUserId(), principal.getUsername(), principal.getFullName())
111+
}
112+
113+
if (userDisplayName == null) {
114+
call.respondError(HttpStatusCode.InternalServerError, "Unable to resolve user display name from token.")
115+
return@delete
116+
}
117+
118+
val archivedDefinition = vulnerabilityResolutionDefinitionService.archive(id, userDisplayName).mapToApi()
119+
120+
call.respond(HttpStatusCode.OK, archivedDefinition)
121+
}

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

Lines changed: 103 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.kotest.core.spec.style.WordSpec
2323
import io.kotest.matchers.collections.shouldHaveSize
24+
import io.kotest.matchers.nulls.shouldBeNull
2425
import io.kotest.matchers.nulls.shouldNotBeNull
2526
import io.kotest.matchers.shouldBe
2627

@@ -132,4 +133,106 @@ class VulnerabilityResolutionDefinitionServiceTest : WordSpec({
132133
}
133134
}
134135
}
136+
137+
"archive" should {
138+
"archive a vulnerability resolution definition" {
139+
val userDisplayName = UserDisplayName(
140+
"abc",
141+
"Test",
142+
"Test User"
143+
)
144+
145+
val definitionId = definitionService.create(
146+
RepositoryId(repositoryId),
147+
runId,
148+
userDisplayName,
149+
listOf("CVE-2020-15250"),
150+
VulnerabilityResolutionReason.INEFFECTIVE_VULNERABILITY,
151+
"Comment."
152+
).id
153+
154+
val archivedDefinition = definitionService.archive(definitionId, userDisplayName)
155+
156+
with(archivedDefinition) {
157+
archived shouldBe true
158+
changes shouldHaveSize 2
159+
160+
with(changes.first()) {
161+
user shouldBe userDisplayName
162+
action shouldBe ChangeEventAction.CREATE
163+
}
164+
165+
with(changes.last()) {
166+
user shouldBe userDisplayName
167+
action shouldBe ChangeEventAction.ARCHIVE
168+
}
169+
}
170+
}
171+
172+
"mark the affected runs as outdated" {
173+
val userDisplayName = UserDisplayName(
174+
"abc",
175+
"Test",
176+
"Test User"
177+
)
178+
179+
val definitionId = definitionService.create(
180+
RepositoryId(repositoryId),
181+
runId,
182+
userDisplayName,
183+
listOf("CVE-2020-15250"),
184+
VulnerabilityResolutionReason.INEFFECTIVE_VULNERABILITY,
185+
"Comment."
186+
).id
187+
188+
val run2Id = fixtures.createOrtRun().id
189+
190+
val repositoryConfiguration = fixtures.createRepositoryConfiguration(run2Id)
191+
fixtures.resolvedConfigurationRepository.addResolutions(run2Id, repositoryConfiguration.resolutions)
192+
193+
val run3Id = fixtures.createOrtRun().id
194+
195+
definitionService.archive(definitionId, userDisplayName)
196+
197+
ortRunService.getOrtRun(runId).shouldNotBeNull {
198+
outdated shouldBe true
199+
outdatedMessage shouldBe "Vulnerability resolution definition archived."
200+
}
201+
202+
ortRunService.getOrtRun(run2Id).shouldNotBeNull {
203+
outdated shouldBe true
204+
outdatedMessage shouldBe "Vulnerability resolution definition archived."
205+
}
206+
207+
ortRunService.getOrtRun(run3Id).shouldNotBeNull {
208+
outdated shouldBe false
209+
outdatedMessage.shouldBeNull()
210+
}
211+
}
212+
}
213+
214+
"getById" should {
215+
"return the vulnerability resolution definition if it exists" {
216+
val userDisplayName = UserDisplayName(
217+
"abc",
218+
"Test",
219+
"Test User"
220+
)
221+
222+
val definition = definitionService.create(
223+
RepositoryId(repositoryId),
224+
runId,
225+
userDisplayName,
226+
listOf("CVE-2020-15250"),
227+
VulnerabilityResolutionReason.INEFFECTIVE_VULNERABILITY,
228+
"Comment."
229+
)
230+
231+
definitionService.getById(definition.id) shouldBe definition
232+
}
233+
234+
"return null if the vulnerability resolution definition doesn't exist" {
235+
definitionService.getById(1) shouldBe null
236+
}
237+
}
135238
})

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.PostVulnerabilityRe
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+
"DeleteVulnerabilityResolution" 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)