Skip to content

Commit fcffcab

Browse files
committed
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 d0e3d1c commit fcffcab

File tree

3 files changed

+348
-0
lines changed

3 files changed

+348
-0
lines changed

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

Lines changed: 91 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
@@ -91,6 +99,7 @@ class ScanOss(
9199
val scanoss = Scanner.builder()
92100
.url(config.apiUrl.removeSuffix("/") + "/scan/direct")
93101
.apiKey(config.apiKey.value)
102+
.settings(buildSettingsFromORTContext(context))
94103
.filterConfig(filterConfig)
95104
.build()
96105

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

0 commit comments

Comments
 (0)