Skip to content

Commit 77293c6

Browse files
isasmendiagussschuberth
authored andcommitted
feat(scanoss): Add snippet choice parsing for scan results
Implement snippet choice processing functionality. It handles findings according to two different scenarios: - Original findings that should be included - Non-relevant findings that should be removed The implementation converts ORT's SnippetChoices into SCANOSS-specific rule types. Signed-off-by: Agustin Isasmendi <[email protected]>
1 parent 6adc221 commit 77293c6

File tree

3 files changed

+338
-0
lines changed

3 files changed

+338
-0
lines changed

plugins/scanners/scanoss/src/main/kotlin/ScanOss.kt

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ package org.ossreviewtoolkit.plugins.scanners.scanoss
2121

2222
import com.scanoss.Scanner
2323
import com.scanoss.filters.FilterConfig
24+
import com.scanoss.settings.Bom
25+
import com.scanoss.settings.RemoveRule
26+
import com.scanoss.settings.ReplaceRule
27+
import com.scanoss.settings.Rule
28+
import com.scanoss.settings.ScanossSettings
2429
import com.scanoss.utils.JsonUtils
2530
import com.scanoss.utils.PackageDetails
2631

@@ -30,6 +35,9 @@ import java.time.Instant
3035
import org.apache.logging.log4j.kotlin.logger
3136

3237
import org.ossreviewtoolkit.model.ScanSummary
38+
import org.ossreviewtoolkit.model.config.SnippetChoices
39+
import org.ossreviewtoolkit.model.config.snippet.SnippetChoice
40+
import org.ossreviewtoolkit.model.config.snippet.SnippetChoiceReason
3341
import org.ossreviewtoolkit.plugins.api.OrtPlugin
3442
import org.ossreviewtoolkit.plugins.api.PluginDescriptor
3543
import org.ossreviewtoolkit.scanner.PathScannerWrapper
@@ -87,6 +95,7 @@ class ScanOss(
8795

8896
// Build the scanner at function level in case any path-specific settings or filters are needed later.
8997
val scanoss = scanossBuilder
98+
.settings(buildSettingsFromORTContext(context))
9099
.filterConfig(filterConfig)
91100
.build()
92101

@@ -99,4 +108,70 @@ class ScanOss(
99108
val endTime = Instant.now()
100109
return generateSummary(startTime, endTime, results)
101110
}
111+
112+
data class ProcessedRules(
113+
val includeRules: List<Rule>,
114+
val ignoreRules: List<Rule>,
115+
val replaceRules: List<ReplaceRule>,
116+
val removeRules: List<RemoveRule>
117+
)
118+
119+
private fun buildSettingsFromORTContext(context: ScanContext): ScanossSettings {
120+
val rules = processSnippetChoices(context.snippetChoices)
121+
val bom = Bom.builder()
122+
.ignore(rules.ignoreRules)
123+
.include(rules.includeRules)
124+
.replace(rules.replaceRules)
125+
.remove(rules.removeRules)
126+
.build()
127+
return ScanossSettings.builder().bom(bom).build()
128+
}
129+
130+
fun processSnippetChoices(snippetChoices: List<SnippetChoices>): ProcessedRules {
131+
val includeRules = mutableListOf<Rule>()
132+
val ignoreRules = mutableListOf<Rule>()
133+
val replaceRules = mutableListOf<ReplaceRule>()
134+
val removeRules = mutableListOf<RemoveRule>()
135+
136+
snippetChoices.forEach { snippetChoice ->
137+
snippetChoice.choices.forEach { choice ->
138+
when (choice.choice.reason) {
139+
SnippetChoiceReason.ORIGINAL_FINDING -> {
140+
includeRules.includeFinding(choice)
141+
}
142+
143+
SnippetChoiceReason.NO_RELEVANT_FINDING -> {
144+
removeRules.removeFinding(choice)
145+
}
146+
147+
SnippetChoiceReason.OTHER -> {
148+
logger.info {
149+
"Encountered OTHER reason for snippet choice in file ${choice.given.sourceLocation.path}"
150+
}
151+
}
152+
}
153+
}
154+
}
155+
156+
return ProcessedRules(includeRules, ignoreRules, replaceRules, removeRules)
157+
}
158+
159+
private fun MutableList<Rule>.includeFinding(choice: SnippetChoice) {
160+
this += Rule.builder()
161+
.purl(choice.choice.purl)
162+
.path(choice.given.sourceLocation.path)
163+
.build()
164+
}
165+
166+
private fun MutableList<RemoveRule>.removeFinding(choice: SnippetChoice) {
167+
this += RemoveRule.builder().apply {
168+
path(choice.given.sourceLocation.path)
169+
170+
// Set line range only if both line positions (startLine and endLine) are known.
171+
if (choice.given.sourceLocation.hasLineRange) {
172+
startLine(choice.given.sourceLocation.startLine)
173+
endLine(choice.given.sourceLocation.endLine)
174+
}
175+
}.build()
176+
}
102177
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/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.ossreviewtoolkit.plugins.scanners.scanoss
21+
22+
import io.kotest.core.spec.style.WordSpec
23+
import io.kotest.matchers.collections.beEmpty
24+
import io.kotest.matchers.collections.shouldBeSingleton
25+
import io.kotest.matchers.should
26+
import io.kotest.matchers.shouldBe
27+
28+
import org.ossreviewtoolkit.model.TextLocation
29+
import org.ossreviewtoolkit.model.config.SnippetChoices
30+
import org.ossreviewtoolkit.model.config.snippet.Choice
31+
import org.ossreviewtoolkit.model.config.snippet.Given
32+
import org.ossreviewtoolkit.model.config.snippet.Provenance
33+
import org.ossreviewtoolkit.model.config.snippet.SnippetChoice
34+
import org.ossreviewtoolkit.model.config.snippet.SnippetChoiceReason
35+
36+
// Sample files in the results.
37+
private const val FILE_1 = "a.java"
38+
private const val FILE_2 = "b.java"
39+
40+
// A sample purl in the results.
41+
private const val PURL_1 = "pkg:github/fakeuser/[email protected]"
42+
43+
class ScanOssTest : WordSpec({
44+
"processSnippetChoices()" should {
45+
"create empty rules when no snippet choices exist" {
46+
val scanoss = createScanOss(createScanOssConfig())
47+
48+
val emptySnippetChoices: List<SnippetChoices> = listOf()
49+
50+
val rules = scanoss.processSnippetChoices(emptySnippetChoices)
51+
52+
rules.ignoreRules should beEmpty()
53+
rules.removeRules should beEmpty()
54+
rules.includeRules should beEmpty()
55+
rules.replaceRules should beEmpty()
56+
}
57+
58+
"create an include rule for snippet choices with ORIGINAL finding" {
59+
val vcsInfo = createVcsInfo()
60+
val scanoss = createScanOss(createScanOssConfig())
61+
62+
val location = TextLocation(FILE_1, 10, 20)
63+
val snippetChoices = createSnippetChoices(
64+
vcsInfo.url,
65+
createSnippetChoice(
66+
location,
67+
PURL_1,
68+
"This is an original finding"
69+
)
70+
)
71+
72+
val rules = scanoss.processSnippetChoices(snippetChoices)
73+
74+
rules.includeRules.shouldBeSingleton { rule ->
75+
rule.purl shouldBe PURL_1
76+
rule.path shouldBe FILE_1
77+
}
78+
79+
rules.removeRules should beEmpty()
80+
rules.ignoreRules should beEmpty()
81+
rules.replaceRules should beEmpty()
82+
}
83+
84+
"create a remove rule for snippet choices with NOT_FINDING reason" {
85+
val vcsInfo = createVcsInfo()
86+
87+
val scanoss = createScanOss(createScanOssConfig())
88+
89+
val location = TextLocation(FILE_2, 15, 30)
90+
val snippetChoices = createSnippetChoices(
91+
vcsInfo.url,
92+
createSnippetChoice(
93+
location,
94+
null, // null PURL for NOT_FINDING.
95+
"This is not a relevant finding"
96+
)
97+
)
98+
99+
val rules = scanoss.processSnippetChoices(snippetChoices)
100+
101+
rules.removeRules.shouldBeSingleton { rule ->
102+
rule.path shouldBe FILE_2
103+
rule.startLine shouldBe 15
104+
rule.endLine shouldBe 30
105+
}
106+
107+
rules.includeRules should beEmpty()
108+
rules.ignoreRules should beEmpty()
109+
rules.replaceRules should beEmpty()
110+
}
111+
112+
"handle multiple snippet choices with different reasons correctly" {
113+
val vcsInfo = createVcsInfo()
114+
val scanoss = createScanOss(createScanOssConfig())
115+
116+
val location1 = TextLocation(FILE_1, 10, 20)
117+
val location2 = TextLocation(FILE_2, 15, 30)
118+
119+
val snippetChoices = createSnippetChoices(
120+
vcsInfo.url,
121+
createSnippetChoice(
122+
location1,
123+
PURL_1,
124+
"This is an original finding"
125+
),
126+
createSnippetChoice(
127+
location2,
128+
null,
129+
"This is not a relevant finding"
130+
)
131+
)
132+
133+
val rules = scanoss.processSnippetChoices(snippetChoices)
134+
135+
rules.includeRules.shouldBeSingleton { rule ->
136+
rule.purl shouldBe PURL_1
137+
rule.path shouldBe FILE_1
138+
}
139+
140+
rules.removeRules.shouldBeSingleton { rule ->
141+
rule.path shouldBe FILE_2
142+
rule.startLine shouldBe 15
143+
rule.endLine shouldBe 30
144+
}
145+
146+
rules.ignoreRules should beEmpty()
147+
rules.replaceRules should beEmpty()
148+
}
149+
150+
"create a remove rule without line ranges when snippet choice has UNKNOWN_LINE (-1) values" {
151+
val vcsInfo = createVcsInfo()
152+
val scanoss = createScanOss(createScanOssConfig())
153+
154+
// Create a TextLocation with -1 for start and end lines.
155+
val location = TextLocation(FILE_2, TextLocation.UNKNOWN_LINE, TextLocation.UNKNOWN_LINE)
156+
val snippetChoices = createSnippetChoices(
157+
vcsInfo.url,
158+
createSnippetChoice(
159+
location,
160+
null, // null PURL for NOT_FINDING.
161+
"This is a not relevant finding with no line ranges"
162+
)
163+
)
164+
165+
val rules = scanoss.processSnippetChoices(snippetChoices)
166+
167+
rules.removeRules.shouldBeSingleton { rule ->
168+
rule.path shouldBe FILE_2
169+
rule.startLine shouldBe null
170+
rule.endLine shouldBe null
171+
}
172+
173+
rules.includeRules should beEmpty()
174+
rules.ignoreRules should beEmpty()
175+
rules.replaceRules should beEmpty()
176+
}
177+
}
178+
})
179+
180+
private fun createSnippetChoices(provenanceUrl: String, vararg snippetChoices: SnippetChoice) =
181+
listOf(SnippetChoices(Provenance(provenanceUrl), snippetChoices.asList()))
182+
183+
private fun createSnippetChoice(location: TextLocation, purl: String? = null, comment: String) =
184+
SnippetChoice(
185+
Given(
186+
location
187+
),
188+
Choice(
189+
purl,
190+
if (purl == null) SnippetChoiceReason.NO_RELEVANT_FINDING else SnippetChoiceReason.ORIGINAL_FINDING,
191+
comment
192+
)
193+
)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/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.ossreviewtoolkit.plugins.scanners.scanoss
21+
22+
import com.scanoss.rest.ScanApi
23+
24+
import org.ossreviewtoolkit.model.VcsInfo
25+
import org.ossreviewtoolkit.model.VcsType
26+
import org.ossreviewtoolkit.plugins.api.Secret
27+
28+
// A test project name.
29+
internal const val PROJECT = "scanoss-test-project"
30+
31+
// A (resolved) test revision.
32+
private const val REVISION = "0123456789012345678901234567890123456789"
33+
34+
/**
35+
* Create a new [ScanOss] instance with the specified [config].
36+
*/
37+
internal fun createScanOss(config: ScanOssConfig): ScanOss = ScanOss(config = config)
38+
39+
/**
40+
* Create a standard [ScanOssConfig] whose properties can be partly specified.
41+
*/
42+
internal fun createScanOssConfig(
43+
apiUrl: String = ScanApi.DEFAULT_BASE_URL,
44+
apiKey: Secret = Secret(""),
45+
regScannerName: String? = null,
46+
minVersion: String? = null,
47+
maxVersion: String? = null,
48+
readFromStorage: Boolean = true,
49+
writeToStorage: Boolean = true
50+
): ScanOssConfig =
51+
ScanOssConfig(
52+
apiUrl = apiUrl,
53+
apiKey = apiKey,
54+
regScannerName = regScannerName,
55+
minVersion = minVersion,
56+
maxVersion = maxVersion,
57+
readFromStorage = readFromStorage,
58+
writeToStorage = writeToStorage
59+
)
60+
61+
/**
62+
* Create a [VcsInfo] object for a project with the given [name][projectName] and the optional parameters for [type],
63+
* [path], and [revision].
64+
*/
65+
internal fun createVcsInfo(
66+
projectName: String = PROJECT,
67+
type: VcsType = VcsType.GIT,
68+
path: String = "",
69+
revision: String = REVISION
70+
): VcsInfo = VcsInfo(type = type, path = path, revision = revision, url = "https://github.com/test/$projectName.git")

0 commit comments

Comments
 (0)