Skip to content

Commit cf78184

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

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: 37 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,27 @@ 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+
ortRunService.markAsOutdated(
75+
getAffectedRuns(id),
76+
"Vulnerability resolution definition archived."
77+
)
78+
79+
VulnerabilityResolutionDefinitionsTable.get(id)
80+
}
81+
82+
suspend fun getById(id: Long): VulnerabilityResolutionDefinition? = db.dbQuery {
83+
VulnerabilityResolutionDefinitionsTable
84+
.getOrNull(id)
85+
}
86+
6387
private fun addChangeLogEvent(
6488
entityId: Long,
6589
action: ChangeEventAction,
@@ -75,4 +99,17 @@ class VulnerabilityResolutionDefinitionService(private val db: Database, private
7599
action
76100
)
77101
}
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+
}
78115
}

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