Skip to content

Commit de95600

Browse files
committed
feat: Allow to update vulnerability resolution definitions
Relates to #1009. Signed-off-by: Johanna Lamppu <[email protected]>
1 parent fedcbff commit de95600

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 definition.
29+
*/
30+
@Serializable
31+
data class UpdateVulnerabilityResolutionDefinition(
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: 25 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

@@ -95,6 +96,30 @@ class VulnerabilityResolutionDefinitionService(private val db: Database, private
9596
VulnerabilityResolutionDefinitionsTable.get(id)
9697
}
9798

99+
suspend fun update(
100+
id: Long,
101+
userDisplayName: UserDisplayName,
102+
idMatchers: OptionalValue<List<String>> = OptionalValue.Absent,
103+
reason: OptionalValue<VulnerabilityResolutionReason> = OptionalValue.Absent,
104+
comment: OptionalValue<String> = OptionalValue.Absent
105+
): VulnerabilityResolutionDefinition = db.dbQuery {
106+
VulnerabilityResolutionDefinitionsTable.updateDefinition(
107+
id,
108+
idMatchers,
109+
reason,
110+
comment
111+
)
112+
113+
addChangeLogEvent(id, ChangeEventAction.UPDATE, userDisplayName)
114+
115+
ortRunService.markAsOutdated(
116+
getAffectedRuns(id),
117+
"Vulnerability resolution definition updated."
118+
)
119+
120+
VulnerabilityResolutionDefinitionsTable.get(id)
121+
}
122+
98123
suspend fun getById(id: Long): VulnerabilityResolutionDefinition? = db.dbQuery {
99124
VulnerabilityResolutionDefinitionsTable
100125
.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.deleteVulnerabilityResolutionDefinition
25+
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.patchVulnerabilityResolutionDefinition
2526
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.postVulnerabilityResolutionDefinition
2627
import org.eclipse.apoapsis.ortserver.components.resolutions.routes.vulnerabilities.restoreVulnerabilityResolutionDefinition
2728
import org.eclipse.apoapsis.ortserver.services.ortrun.OrtRunService
@@ -34,4 +35,5 @@ fun Route.resolutionsRoutes(
3435
postVulnerabilityResolutionDefinition(ortRunService, vulnerabilityResolutionDefinitionService)
3536
deleteVulnerabilityResolutionDefinition(vulnerabilityResolutionDefinitionService)
3637
restoreVulnerabilityResolutionDefinition(vulnerabilityResolutionDefinitionService)
38+
patchVulnerabilityResolutionDefinition(vulnerabilityResolutionDefinitionService)
3739
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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.UpdateVulnerabilityResolutionDefinition
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.patchVulnerabilityResolutionDefinition(
55+
vulnerabilityResolutionDefinitionService: VulnerabilityResolutionDefinitionService
56+
) = patch("/resolutions/vulnerabilities/{id}", {
57+
operationId = "PatchVulnerabilityResolutionDefinition"
58+
summary = "Update a vulnerability resolution definition"
59+
tags = listOf("Resolutions")
60+
61+
request {
62+
pathParameter<Long>("id") {
63+
description = "The ID of the vulnerability resolution definition"
64+
}
65+
66+
jsonBody<UpdateVulnerabilityResolutionDefinition> {
67+
description = "Set the values that should be updated."
68+
example("Update Vulnerability Resolution Definition") {
69+
value = UpdateVulnerabilityResolutionDefinition(
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 Definition") {
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 definition is archived."
109+
}
110+
}
111+
}) {
112+
val id = call.requireIdParameter("id")
113+
114+
val definition = vulnerabilityResolutionDefinitionService.getById(id)
115+
116+
if (definition == null) throw AuthorizationException()
117+
118+
requirePermission(RepositoryPermission.WRITE.roleName(definition.hierarchyId.value))
119+
120+
if (definition.archived) {
121+
call.respondError(HttpStatusCode.Conflict, "The requested vulnerability resolution definition is archived.")
122+
return@patch
123+
}
124+
125+
val updateResolution = call.receive<UpdateVulnerabilityResolutionDefinition>()
126+
127+
// Extract the user information from the principal.
128+
val userDisplayName = call.principal<OrtPrincipal>()?.let { principal ->
129+
ModelUserDisplayName(principal.getUserId(), principal.getUsername(), principal.getFullName())
130+
}
131+
132+
if (userDisplayName == null) {
133+
call.respondError(HttpStatusCode.InternalServerError, "Unable to resolve user display name from token.")
134+
return@patch
135+
}
136+
137+
val updatedDefinition = vulnerabilityResolutionDefinitionService.update(
138+
id,
139+
userDisplayName,
140+
updateResolution.idMatchers.mapToModel(),
141+
updateResolution.reason.mapToModel { it.mapToModel() },
142+
updateResolution.comment.mapToModel()
143+
).mapToApi()
144+
145+
call.respond(HttpStatusCode.OK, updatedDefinition)
146+
}

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 "New vulnerability resolution added."
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)