Skip to content

Commit a1cd87e

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 457657b commit a1cd87e

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
@@ -89,6 +97,7 @@ class ScanOss(
8997

9098
// Build the scanner at function level in case any path-specific settings or filters are needed later
9199
val scanoss = scanossBuilder
100+
.settings(buildSettingsFromORTContext(context))
92101
.filterConfig(filterConfig)
93102
.build()
94103

@@ -101,4 +110,86 @@ class ScanOss(
101110
val endTime = Instant.now()
102111
return generateSummary(startTime, endTime, results)
103112
}
113+
114+
data class ProcessedRules(
115+
val includeRules: List<Rule>,
116+
val ignoreRules: List<Rule>,
117+
val replaceRules: List<ReplaceRule>,
118+
val removeRules: List<RemoveRule>
119+
)
120+
121+
private fun buildSettingsFromORTContext(context: ScanContext): ScanossSettings {
122+
val rules = processSnippetChoices(context.snippetChoices)
123+
val bom = Bom.builder()
124+
.ignore(rules.ignoreRules)
125+
.include(rules.includeRules)
126+
.replace(rules.replaceRules)
127+
.remove(rules.removeRules)
128+
.build()
129+
return ScanossSettings.builder().bom(bom).build()
130+
}
131+
132+
fun processSnippetChoices(snippetChoices: List<SnippetChoices>): ProcessedRules {
133+
val includeRules = mutableListOf<Rule>()
134+
val ignoreRules = mutableListOf<Rule>()
135+
val replaceRules = mutableListOf<ReplaceRule>()
136+
val removeRules = mutableListOf<RemoveRule>()
137+
138+
snippetChoices.forEach { snippetChoice ->
139+
snippetChoice.choices.forEach { choice ->
140+
when (choice.choice.reason) {
141+
SnippetChoiceReason.ORIGINAL_FINDING -> {
142+
processOriginalFinding(
143+
choice = choice,
144+
includeRules = includeRules
145+
)
146+
}
147+
148+
SnippetChoiceReason.NO_RELEVANT_FINDING -> {
149+
processNoRelevantFinding(
150+
choice = choice,
151+
removeRules = removeRules
152+
)
153+
}
154+
155+
SnippetChoiceReason.OTHER -> {
156+
processOtherReason(choice)
157+
}
158+
}
159+
}
160+
}
161+
162+
return ProcessedRules(includeRules, ignoreRules, replaceRules, removeRules)
163+
}
164+
165+
private fun processOriginalFinding(choice: SnippetChoice, includeRules: MutableList<Rule>) {
166+
includeRules.add(
167+
Rule.builder()
168+
.purl(choice.choice.purl)
169+
.path(choice.given.sourceLocation.path)
170+
.build()
171+
)
172+
}
173+
174+
private fun processNoRelevantFinding(choice: SnippetChoice, removeRules: MutableList<RemoveRule>) {
175+
val builder = RemoveRule.builder()
176+
177+
builder.path(choice.given.sourceLocation.path)
178+
179+
// Set line range only if both start and end lines are positive numbers
180+
// If either line is < 0, no line range is set and the rule will apply to the entire file
181+
if (choice.given.sourceLocation.startLine > 0 && choice.given.sourceLocation.endLine > 0) {
182+
builder.startLine(choice.given.sourceLocation.startLine)
183+
builder.endLine(choice.given.sourceLocation.endLine)
184+
}
185+
186+
val rule = builder.build()
187+
removeRules.add(rule)
188+
}
189+
190+
private fun processOtherReason(snippetChoice: SnippetChoice) {
191+
logger.info {
192+
"Encountered OTHER reason for snippet choice in file ${snippetChoice.given.sourceLocation.path}"
193+
}
194+
}
104195
}
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)