Skip to content

Commit bf851a6

Browse files
committed
feat: Allow to restore vulnerability resolutions
Allow to restore a previously archived resolution. After the restoration the resolution will again be injected to the repository configuration of new runs. Relates to #1009. Signed-off-by: Johanna Lamppu <[email protected]>
1 parent 0bebd03 commit bf851a6

File tree

6 files changed

+375
-0
lines changed

6 files changed

+375
-0
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@ class VulnerabilityResolutionDefinitionService(private val db: Database, private
8181
definition
8282
}
8383

84+
suspend fun restore(id: Long, userDisplayName: UserDisplayName): VulnerabilityResolutionDefinition = db.dbQuery {
85+
VulnerabilityResolutionDefinitionsTable.updateDefinition(
86+
id,
87+
archivedInput = false.asPresent()
88+
)
89+
90+
addChangeLogEvent(id, ChangeEventAction.RESTORE, userDisplayName)
91+
92+
val definition = VulnerabilityResolutionDefinitionsTable.get(id)
93+
94+
ortRunService.markAsOutdated(
95+
getAffectedRuns(id) + definition.contextRunId,
96+
"Vulnerability resolution definition restored."
97+
)
98+
99+
definition
100+
}
101+
84102
suspend fun getById(id: Long): VulnerabilityResolutionDefinition? = db.dbQuery {
85103
VulnerabilityResolutionDefinitionsTable
86104
.getOrNull(id)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import io.ktor.server.routing.Route
2323

2424
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.deleteVulnerabilityResolution
2525
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.postVulnerabilityResolution
26+
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.restoreVulnerabilityResolution
2627
import org.eclipse.apoapsis.ortserver.services.ortrun.OrtRunService
2728

2829
/** Add all resolutions routes. */
@@ -32,4 +33,5 @@ fun Route.resolutionsRoutes(
3233
) {
3334
postVulnerabilityResolution(ortRunService, vulnerabilityResolutionDefinitionService)
3435
deleteVulnerabilityResolution(vulnerabilityResolutionDefinitionService)
36+
restoreVulnerabilityResolution(vulnerabilityResolutionDefinitionService)
3537
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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.post
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.restoreVulnerabilityResolution(
51+
vulnerabilityResolutionDefinitionService: VulnerabilityResolutionDefinitionService
52+
) = post("/resolutions/vulnerabilities/{id}/restore", {
53+
operationId = "restoreVulnerabilityResolution"
54+
summary = "Restore 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+
jsonBody<VulnerabilityResolutionDefinition> {
67+
example("Restore Vulnerability Resolution") {
68+
value = VulnerabilityResolutionDefinition(
69+
id = 1,
70+
idMatchers = listOf("CVE-2020-15250", "GHSA-269g-pwp5-87pp"),
71+
reason = VulnerabilityResolutionReason.INEFFECTIVE_VULNERABILITY,
72+
comment = "Comment",
73+
archived = false,
74+
changes = listOf(
75+
ChangeEvent(
76+
user = UserDisplayName(username = "User"),
77+
occurredAt = Instant.parse("2024-01-01T00:00:00Z"),
78+
ChangeEventAction.CREATE
79+
),
80+
ChangeEvent(
81+
user = UserDisplayName(username = "User"),
82+
occurredAt = Instant.parse("2024-01-03T00:00:00Z"),
83+
ChangeEventAction.ARCHIVE
84+
),
85+
ChangeEvent(
86+
user = UserDisplayName(username = "User"),
87+
occurredAt = Instant.parse("2024-01-04T00:00:00Z"),
88+
ChangeEventAction.RESTORE
89+
)
90+
)
91+
)
92+
}
93+
}
94+
}
95+
96+
HttpStatusCode.NoContent to {
97+
description = "The vulnerability resolution is not archived."
98+
}
99+
}
100+
}) {
101+
val id = call.requireIdParameter("id")
102+
103+
val definition = vulnerabilityResolutionDefinitionService.getById(id)
104+
105+
if (definition == null) throw AuthorizationException()
106+
107+
requirePermission(RepositoryPermission.WRITE.roleName(definition.hierarchyId.value))
108+
109+
if (!definition.archived) {
110+
call.respond(HttpStatusCode.NoContent, "The vulnerability resolution is not archived.")
111+
return@post
112+
}
113+
114+
// Extract the user information from the principal.
115+
val userDisplayName = call.principal<OrtPrincipal>()?.let { principal ->
116+
ModelUserDisplayName(principal.getUserId(), principal.getUsername(), principal.getFullName())
117+
}
118+
119+
if (userDisplayName == null) {
120+
call.respondError(HttpStatusCode.InternalServerError, "Unable to resolve user display name from token.")
121+
return@post
122+
}
123+
124+
val vulnerabilityResolutionDefinition =
125+
vulnerabilityResolutionDefinitionService.restore(id, userDisplayName).mapToApi()
126+
127+
call.respond(HttpStatusCode.OK, vulnerabilityResolutionDefinition)
128+
}

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,91 @@ class VulnerabilityResolutionDefinitionServiceTest : WordSpec({
211211
}
212212
}
213213

214+
"restore" should {
215+
"restore an archived vulnerability resolution definition" {
216+
val userDisplayName = UserDisplayName(
217+
"abc",
218+
"Test",
219+
"Test User"
220+
)
221+
222+
val definitionId = definitionService.create(
223+
RepositoryId(repositoryId),
224+
runId,
225+
userDisplayName,
226+
listOf("CVE-2020-15250"),
227+
VulnerabilityResolutionReason.INEFFECTIVE_VULNERABILITY,
228+
"Comment."
229+
).id
230+
231+
definitionService.archive(definitionId, userDisplayName)
232+
233+
val restoredDefinition = definitionService.restore(definitionId, userDisplayName)
234+
235+
with(restoredDefinition) {
236+
archived shouldBe false
237+
changes shouldHaveSize 3
238+
239+
with(changes[0]) {
240+
user shouldBe userDisplayName
241+
action shouldBe ChangeEventAction.CREATE
242+
}
243+
244+
with(changes[1]) {
245+
user shouldBe userDisplayName
246+
action shouldBe ChangeEventAction.ARCHIVE
247+
}
248+
249+
with(changes[2]) {
250+
user shouldBe userDisplayName
251+
action shouldBe ChangeEventAction.RESTORE
252+
}
253+
}
254+
}
255+
256+
"mark the affected runs as outdated" {
257+
val userDisplayName = UserDisplayName(
258+
"abc",
259+
"Test",
260+
"Test User"
261+
)
262+
263+
val definitionId = definitionService.create(
264+
RepositoryId(repositoryId),
265+
runId,
266+
userDisplayName,
267+
listOf("CVE-2020-15250"),
268+
VulnerabilityResolutionReason.INEFFECTIVE_VULNERABILITY,
269+
"Comment."
270+
).id
271+
272+
val run2Id = fixtures.createOrtRun().id
273+
274+
val repositoryConfiguration = fixtures.createRepositoryConfiguration(run2Id)
275+
fixtures.resolvedConfigurationRepository.addResolutions(run2Id, repositoryConfiguration.resolutions)
276+
277+
val run3Id = fixtures.createOrtRun().id
278+
279+
definitionService.archive(definitionId, userDisplayName)
280+
definitionService.restore(definitionId, userDisplayName)
281+
282+
ortRunService.getOrtRun(runId).shouldNotBeNull {
283+
outdated shouldBe true
284+
outdatedMessage shouldBe "Vulnerability resolution definition restored."
285+
}
286+
287+
ortRunService.getOrtRun(run2Id).shouldNotBeNull {
288+
outdated shouldBe true
289+
outdatedMessage shouldBe "Vulnerability resolution definition restored."
290+
}
291+
292+
ortRunService.getOrtRun(run3Id).shouldNotBeNull {
293+
outdated shouldBe false
294+
outdatedMessage.shouldBeNull()
295+
}
296+
}
297+
}
298+
214299
"getById" should {
215300
"return the vulnerability resolution definition if it exists" {
216301
val userDisplayName = UserDisplayName(

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,37 @@ class ResolutionsAuthorizationTest : AbstractAuthorizationTest({
148148
}
149149
}
150150
}
151+
152+
"RestoreVulnerabilityResolution" should {
153+
"require role RepositoryPermission.WRITE.roleName(repositoryId)" {
154+
val userDisplayName = UserDisplayName("abc", "Test")
155+
156+
val definitionId = definitionService.create(
157+
RepositoryId(repositoryId),
158+
runId,
159+
userDisplayName,
160+
createBody.idMatchers,
161+
createBody.reason.mapToModel(),
162+
createBody.comment
163+
).id
164+
165+
definitionService.archive(definitionId, userDisplayName)
166+
167+
requestShouldRequireRole(
168+
routes = { resolutionsRoutes(ortRunService, definitionService) },
169+
role = RepositoryPermission.WRITE.roleName(repositoryId)
170+
) {
171+
post("/resolutions/vulnerabilities/$definitionId/restore")
172+
}
173+
}
174+
175+
"respond with 'Forbidden' when repository ID cannot be resolved" {
176+
requestShouldRequireAuthentication(
177+
routes = { resolutionsRoutes(ortRunService, definitionService) },
178+
successStatus = HttpStatusCode.Forbidden
179+
) {
180+
post("/resolutions/vulnerabilities/9999/restore")
181+
}
182+
}
183+
}
151184
})

0 commit comments

Comments
 (0)