Skip to content

Commit 60346f6

Browse files
committed
feat: Allow to update vulnerability resolutions
Updating a vulnerability resolution will update the definition, and the new updated values will be used when injecting the resolution on new runs. Old runs where the resolution was injected to previously will still retain the information of what the resolution was when the run was made. Relates to #1009. Signed-off-by: Johanna Lamppu <[email protected]>
1 parent f8c3fb4 commit 60346f6

File tree

7 files changed

+468
-0
lines changed

7 files changed

+468
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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
21+
22+
import kotlinx.serialization.Serializable
23+
24+
import org.eclipse.apoapsis.ortserver.shared.apimodel.OptionalValue
25+
import org.eclipse.apoapsis.ortserver.shared.apimodel.VulnerabilityResolutionReason
26+
27+
/**
28+
* The request object for updating a vulnerability resolution.
29+
*/
30+
@Serializable
31+
data class PatchVulnerabilityResolution(
32+
/**
33+
* The list of vulnerability ID matchers (regular expressions) to match the ids of the vulnerabilities to resolve.
34+
*/
35+
val idMatchers: OptionalValue<List<String>> = OptionalValue.Absent,
36+
37+
/** The reason why the vulnerability is resolved. */
38+
val reason: OptionalValue<VulnerabilityResolutionReason> = OptionalValue.Absent,
39+
40+
/** A comment to further explain why the [reason] is applicable here. */
41+
val comment: OptionalValue<String> = OptionalValue.Absent
42+
)

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.eclipse.apoapsis.ortserver.model.RepositoryId
3131
import org.eclipse.apoapsis.ortserver.model.UserDisplayName
3232
import org.eclipse.apoapsis.ortserver.model.VulnerabilityResolutionDefinition
3333
import org.eclipse.apoapsis.ortserver.model.VulnerabilityResolutionReason
34+
import org.eclipse.apoapsis.ortserver.model.util.OptionalValue
3435
import org.eclipse.apoapsis.ortserver.model.util.asPresent
3536
import org.eclipse.apoapsis.ortserver.services.ortrun.OrtRunService
3637

@@ -99,6 +100,32 @@ class VulnerabilityResolutionDefinitionService(private val db: Database, private
99100
definition
100101
}
101102

103+
suspend fun update(
104+
id: Long,
105+
userDisplayName: UserDisplayName,
106+
idMatchers: OptionalValue<List<String>> = OptionalValue.Absent,
107+
reason: OptionalValue<VulnerabilityResolutionReason> = OptionalValue.Absent,
108+
comment: OptionalValue<String> = OptionalValue.Absent
109+
): VulnerabilityResolutionDefinition = db.dbQuery {
110+
VulnerabilityResolutionDefinitionsTable.updateDefinition(
111+
id,
112+
idMatchers,
113+
reason,
114+
comment
115+
)
116+
117+
addChangeLogEvent(id, ChangeEventAction.UPDATE, userDisplayName)
118+
119+
val definition = VulnerabilityResolutionDefinitionsTable.get(id)
120+
121+
ortRunService.markAsOutdated(
122+
getAffectedRuns(id) + definition.contextRunId,
123+
"Vulnerability resolution definition updated."
124+
)
125+
126+
definition
127+
}
128+
102129
suspend fun getById(id: Long): VulnerabilityResolutionDefinition? = db.dbQuery {
103130
VulnerabilityResolutionDefinitionsTable
104131
.getOrNull(id)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package org.eclipse.apoapsis.ortserver.components.resolutions
2222
import io.ktor.server.routing.Route
2323

2424
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.deleteVulnerabilityResolution
25+
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.patchVulnerabilityResolution
2526
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.postVulnerabilityResolution
2627
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.restoreVulnerabilityResolution
2728
import org.eclipse.apoapsis.ortserver.services.ortrun.OrtRunService
@@ -34,4 +35,5 @@ fun Route.resolutionsRoutes(
3435
postVulnerabilityResolution(ortRunService, vulnerabilityResolutionDefinitionService)
3536
deleteVulnerabilityResolution(vulnerabilityResolutionDefinitionService)
3637
restoreVulnerabilityResolution(vulnerabilityResolutionDefinitionService)
38+
patchVulnerabilityResolution(vulnerabilityResolutionDefinitionService)
3739
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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.patch
23+
24+
import io.ktor.http.HttpStatusCode
25+
import io.ktor.server.auth.principal
26+
import io.ktor.server.request.receive
27+
import io.ktor.server.response.respond
28+
import io.ktor.server.routing.Route
29+
30+
import kotlinx.datetime.Instant
31+
32+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.AuthorizationException
33+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.OrtPrincipal
34+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getFullName
35+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUserId
36+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.getUsername
37+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.permissions.RepositoryPermission
38+
import org.eclipse.apoapsis.ortserver.components.authorization.keycloak.requirePermission
39+
import org.eclipse.apoapsis.ortserver.components.resolutions.PatchVulnerabilityResolution
40+
import org.eclipse.apoapsis.ortserver.components.resolutions.VulnerabilityResolutionDefinitionService
41+
import org.eclipse.apoapsis.ortserver.model.UserDisplayName as ModelUserDisplayName
42+
import org.eclipse.apoapsis.ortserver.shared.apimappings.mapToApi
43+
import org.eclipse.apoapsis.ortserver.shared.apimappings.mapToModel
44+
import org.eclipse.apoapsis.ortserver.shared.apimodel.ChangeEvent
45+
import org.eclipse.apoapsis.ortserver.shared.apimodel.ChangeEventAction
46+
import org.eclipse.apoapsis.ortserver.shared.apimodel.UserDisplayName
47+
import org.eclipse.apoapsis.ortserver.shared.apimodel.VulnerabilityResolutionDefinition
48+
import org.eclipse.apoapsis.ortserver.shared.apimodel.VulnerabilityResolutionReason
49+
import org.eclipse.apoapsis.ortserver.shared.apimodel.asPresent
50+
import org.eclipse.apoapsis.ortserver.shared.ktorutils.jsonBody
51+
import org.eclipse.apoapsis.ortserver.shared.ktorutils.requireIdParameter
52+
import org.eclipse.apoapsis.ortserver.shared.ktorutils.respondError
53+
54+
internal fun Route.patchVulnerabilityResolution(
55+
vulnerabilityResolutionDefinitionService: VulnerabilityResolutionDefinitionService
56+
) = patch("/resolutions/vulnerabilities/{id}", {
57+
operationId = "patchVulnerabilityResolution"
58+
summary = "Update a vulnerability resolution"
59+
tags = listOf("Resolutions")
60+
61+
request {
62+
pathParameter<Long>("id") {
63+
description = "The ID of the vulnerability resolution definition"
64+
}
65+
66+
jsonBody<PatchVulnerabilityResolution> {
67+
description = "Set the values that should be updated."
68+
example("Update Vulnerability Resolution") {
69+
value = PatchVulnerabilityResolution(
70+
idMatchers = listOf("CVE-2020-15250", "GHSA-269g-pwp5-87pp").asPresent(),
71+
reason = VulnerabilityResolutionReason.WILL_NOT_FIX_VULNERABILITY.asPresent(),
72+
comment = "Updated comment.".asPresent()
73+
)
74+
}
75+
}
76+
}
77+
78+
response {
79+
HttpStatusCode.OK to {
80+
description = "Success"
81+
82+
jsonBody<VulnerabilityResolutionDefinition> {
83+
example("Update Vulnerability Resolution") {
84+
value = VulnerabilityResolutionDefinition(
85+
id = 1,
86+
idMatchers = listOf("CVE-2020-15250", "GHSA-269g-pwp5-87pp"),
87+
reason = VulnerabilityResolutionReason.WILL_NOT_FIX_VULNERABILITY,
88+
comment = "Updated comment.",
89+
archived = false,
90+
changes = listOf(
91+
ChangeEvent(
92+
user = UserDisplayName(username = "User"),
93+
occurredAt = Instant.parse("2024-01-01T00:00:00Z"),
94+
action = ChangeEventAction.CREATE
95+
),
96+
ChangeEvent(
97+
user = UserDisplayName(username = "User"),
98+
occurredAt = Instant.parse("2024-01-02T00:00:00Z"),
99+
action = ChangeEventAction.UPDATE
100+
)
101+
)
102+
)
103+
}
104+
}
105+
}
106+
107+
HttpStatusCode.BadRequest to {
108+
description = "The requested vulnerability resolution is archived."
109+
}
110+
}
111+
}) {
112+
val id = call.requireIdParameter("id")
113+
114+
val definition = vulnerabilityResolutionDefinitionService.getById(id) ?: throw AuthorizationException()
115+
116+
requirePermission(RepositoryPermission.WRITE.roleName(definition.hierarchyId.value))
117+
118+
if (definition.archived) {
119+
call.respondError(HttpStatusCode.Conflict, "The requested vulnerability resolution is archived.")
120+
return@patch
121+
}
122+
123+
val updateResolution = call.receive<PatchVulnerabilityResolution>()
124+
125+
// Extract the user information from the principal.
126+
val userDisplayName = call.principal<OrtPrincipal>()?.let { principal ->
127+
ModelUserDisplayName(principal.getUserId(), principal.getUsername(), principal.getFullName())
128+
}
129+
130+
if (userDisplayName == null) {
131+
call.respondError(HttpStatusCode.InternalServerError, "Unable to resolve user display name from token.")
132+
return@patch
133+
}
134+
135+
val updatedDefinition = vulnerabilityResolutionDefinitionService.update(
136+
id,
137+
userDisplayName,
138+
updateResolution.idMatchers.mapToModel(),
139+
updateResolution.reason.mapToModel { it.mapToModel() },
140+
updateResolution.comment.mapToModel()
141+
).mapToApi()
142+
143+
call.respond(HttpStatusCode.OK, updatedDefinition)
144+
}

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import org.eclipse.apoapsis.ortserver.model.ChangeEventAction
3333
import org.eclipse.apoapsis.ortserver.model.RepositoryId
3434
import org.eclipse.apoapsis.ortserver.model.UserDisplayName
3535
import org.eclipse.apoapsis.ortserver.model.VulnerabilityResolutionReason
36+
import org.eclipse.apoapsis.ortserver.model.util.asPresent
3637
import org.eclipse.apoapsis.ortserver.services.ortrun.OrtRunService
3738

3839
import org.jetbrains.exposed.sql.Database
@@ -296,6 +297,92 @@ class VulnerabilityResolutionDefinitionServiceTest : WordSpec({
296297
}
297298
}
298299

300+
"update" should {
301+
"update a vulnerability resolution definition and add an event in the change log" {
302+
val userDisplayName = UserDisplayName(
303+
"abc",
304+
"Test",
305+
"Test User"
306+
)
307+
308+
val definitionId = definitionService.create(
309+
RepositoryId(repositoryId),
310+
runId,
311+
userDisplayName,
312+
listOf("CVE-2020-15250"),
313+
VulnerabilityResolutionReason.WILL_NOT_FIX_VULNERABILITY,
314+
"Comment."
315+
).id
316+
317+
val updatedDefinition = definitionService.update(
318+
definitionId,
319+
userDisplayName,
320+
listOf("CVE-2020-15250", "GHSA-269g-pwp5-87pp").asPresent(),
321+
VulnerabilityResolutionReason.INEFFECTIVE_VULNERABILITY.asPresent(),
322+
"Updated comment.".asPresent()
323+
)
324+
325+
with(updatedDefinition) {
326+
idMatchers shouldBe listOf("CVE-2020-15250", "GHSA-269g-pwp5-87pp")
327+
reason shouldBe VulnerabilityResolutionReason.INEFFECTIVE_VULNERABILITY
328+
comment shouldBe "Updated comment."
329+
archived shouldBe false
330+
changes shouldHaveSize 2
331+
332+
with(changes.first()) {
333+
user shouldBe userDisplayName
334+
action shouldBe ChangeEventAction.CREATE
335+
}
336+
337+
with(changes.last()) {
338+
user shouldBe userDisplayName
339+
action shouldBe ChangeEventAction.UPDATE
340+
}
341+
}
342+
}
343+
344+
"mark the affected runs as outdated" {
345+
val userDisplayName = UserDisplayName(
346+
"abc",
347+
"Test",
348+
"Test User"
349+
)
350+
351+
val definitionId = definitionService.create(
352+
RepositoryId(repositoryId),
353+
runId,
354+
userDisplayName,
355+
listOf("CVE-2020-15250"),
356+
VulnerabilityResolutionReason.INEFFECTIVE_VULNERABILITY,
357+
"Comment."
358+
).id
359+
360+
val run2Id = fixtures.createOrtRun().id
361+
362+
val repositoryConfiguration = fixtures.createRepositoryConfiguration(run2Id)
363+
fixtures.resolvedConfigurationRepository.addResolutions(run2Id, repositoryConfiguration.resolutions)
364+
365+
val run3Id = fixtures.createOrtRun().id
366+
367+
definitionService.update(definitionId, userDisplayName, comment = "Updated comment.".asPresent())
368+
369+
ortRunService.getOrtRun(runId).shouldNotBeNull {
370+
outdated shouldBe true
371+
outdatedMessage shouldBe "Vulnerability resolution definition updated."
372+
}
373+
374+
ortRunService.getOrtRun(run2Id).shouldNotBeNull {
375+
outdated shouldBe true
376+
outdatedMessage shouldBe "Vulnerability resolution definition updated."
377+
}
378+
379+
ortRunService.getOrtRun(run3Id).shouldNotBeNull {
380+
outdated shouldBe false
381+
outdatedMessage.shouldBeNull()
382+
}
383+
}
384+
}
385+
299386
"getById" should {
300387
"return the vulnerability resolution definition if it exists" {
301388
val userDisplayName = UserDisplayName(

0 commit comments

Comments
 (0)