Skip to content

Commit 4c52f61

Browse files
committed
fix(scanoss): Prevent duplicate licenses in SnippetFindings
Combine same licenses from different sources to avoid duplicates in scan results. Signed-off-by: Agustin Isasmendi <[email protected]>
1 parent 647b3bb commit 4c52f61

File tree

4 files changed

+144
-5
lines changed

4 files changed

+144
-5
lines changed

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,9 @@ private fun getSnippets(details: ScanFileDetails): Set<Snippet> {
144144
val url = requireNotNull(details.url)
145145
val purls = requireNotNull(details.purls)
146146

147-
val licenses = details.licenseDetails.orEmpty().mapTo(mutableSetOf()) { license ->
148-
SpdxExpression.parse(license.name)
149-
}
147+
val license = details.licenseDetails.orEmpty()
148+
.map { license -> SpdxExpression.parse(license.name) }
149+
.toExpression()?.sorted() ?: SpdxLicenseIdExpression(SpdxConstants.NOASSERTION)
150150

151151
val score = matched.substringBeforeLast("%").toFloat()
152152
val locations = convertLines(fileUrl, ossLines)
@@ -157,8 +157,6 @@ private fun getSnippets(details: ScanFileDetails): Set<Snippet> {
157157
return buildSet {
158158
purls.forEach { purl ->
159159
locations.forEach { snippetLocation ->
160-
val license = licenses.toExpression()?.sorted() ?: SpdxLicenseIdExpression(SpdxConstants.NOASSERTION)
161-
162160
add(Snippet(score, snippetLocation, provenance, purl, license))
163161
}
164162
}

plugins/scanners/scanoss/src/test/kotlin/ScanOssResultParserTest.kt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import io.kotest.matchers.collections.containExactlyInAnyOrder
2727
import io.kotest.matchers.collections.haveSize
2828
import io.kotest.matchers.collections.shouldContain
2929
import io.kotest.matchers.should
30+
import io.kotest.matchers.shouldBe
3031

3132
import java.time.Instant
3233

@@ -38,7 +39,9 @@ import org.ossreviewtoolkit.model.SnippetFinding
3839
import org.ossreviewtoolkit.model.TextLocation
3940
import org.ossreviewtoolkit.model.VcsInfo
4041
import org.ossreviewtoolkit.model.VcsType
42+
import org.ossreviewtoolkit.utils.spdx.SpdxConstants
4143
import org.ossreviewtoolkit.utils.spdx.SpdxExpression
44+
import org.ossreviewtoolkit.utils.spdx.SpdxLicenseIdExpression
4245
import org.ossreviewtoolkit.utils.test.readResource
4346

4447
class ScanOssResultParserTest : WordSpec({
@@ -131,5 +134,56 @@ class ScanOssResultParserTest : WordSpec({
131134
)
132135
)
133136
}
137+
138+
"combine the same license from different sources into a single expression" {
139+
// When the same license appears in multiple sources (like scancode and file_header),
140+
// combine them into a single expression rather than duplicating.
141+
val results = readResource("/scanoss-snippet-same-license-multiple-sources.json").let {
142+
JsonUtils.toScanFileResultsFromObject(JsonUtils.toJsonObject(it))
143+
}
144+
145+
val time = Instant.now()
146+
val summary = generateSummary(time, time, results)
147+
148+
// Verify the snippet finding.
149+
summary.snippetFindings should haveSize(1)
150+
val snippet = summary.snippetFindings.first().snippets.first()
151+
152+
// Consolidate the license into a single expression
153+
// even though it came from both "scancode" and "file_header" sources.
154+
snippet.license shouldBe SpdxExpression.parse("LGPL-2.1-or-later")
155+
156+
// Preserve other snippet details correctly.
157+
with(summary.snippetFindings.first()) {
158+
sourceLocation.path shouldBe "src/check_error.c"
159+
sourceLocation.startLine shouldBe 16
160+
sourceLocation.endLine shouldBe 24
161+
}
162+
}
163+
164+
"handle empty license array with NOASSERTION" {
165+
// When a component has an empty licenses array, use NOASSERTION.
166+
167+
val results = readResource("/scanoss-snippet-no-license-data.json").let {
168+
JsonUtils.toScanFileResultsFromObject(JsonUtils.toJsonObject(it))
169+
}
170+
171+
val time = Instant.now()
172+
val summary = generateSummary(time, time, results)
173+
174+
// Verify the snippet finding.
175+
summary.snippetFindings should haveSize(1)
176+
val snippet = summary.snippetFindings.first().snippets.first()
177+
178+
// Use NOASSERTION when no licenses are provided.
179+
snippet.license shouldBe SpdxLicenseIdExpression(SpdxConstants.NOASSERTION)
180+
181+
// Preserve other snippet details correctly.
182+
with(summary.snippetFindings.first()) {
183+
sourceLocation.path shouldBe "fake_file.c"
184+
sourceLocation.startLine shouldBe 16
185+
sourceLocation.endLine shouldBe 24
186+
}
187+
}
134188
}
135189
})
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"fake_file.c": [
3+
{
4+
"component": "check",
5+
"file": "fake_file.c",
6+
"file_hash": "4597ef1de00849bb96d42e78f2cfc3a7",
7+
"file_url": "https://api.scanoss.com/file_contents//4597ef1de00849bb96d42e78f2cfc3a7",
8+
"id": "snippet",
9+
"latest": "0.8.1",
10+
"licenses": [],
11+
"lines": "16-24",
12+
"matched": "15%",
13+
"oss_lines": "34-42",
14+
"purl": [
15+
"pkg:sourceforge/check"
16+
],
17+
"release_date": "2002-03-02",
18+
"server": {
19+
"kb_version": {
20+
"daily": "25.05.14",
21+
"monthly": "25.04"
22+
},
23+
"version": "5.4.10"
24+
},
25+
"source_hash": "74c49597c2934e08b2ce8797f4aa7454",
26+
"status": "pending",
27+
"url": "https://sourceforge.net/projects/check",
28+
"url_hash": "d81953e1dca4c498140c44f5d6fa92d6",
29+
"vendor": "check",
30+
"version": "0.8.1"
31+
}
32+
]
33+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"src/check_error.c": [
3+
{
4+
"component": "check",
5+
"file": "src/check_error.c",
6+
"file_hash": "4597ef1de00849bb96d42e78f2cfc3a7",
7+
"file_url": "https://api.scanoss.com/file_contents//4597ef1de00849bb96d42e78f2cfc3a7",
8+
"id": "snippet",
9+
"latest": "0.8.1",
10+
"licenses": [
11+
{
12+
"checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/LGPL-2.1-or-later.txt",
13+
"copyleft": "yes",
14+
"incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
15+
"name": "LGPL-2.1-or-later",
16+
"osadl_updated": "2025-02-10T14:26:00+0000",
17+
"patent_hints": "yes",
18+
"source": "scancode",
19+
"url": "https://spdx.org/licenses/LGPL-2.1-or-later.html"
20+
},
21+
{
22+
"checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/LGPL-2.1-or-later.txt",
23+
"copyleft": "yes",
24+
"incompatible_with": "Apache-1.0, Apache-1.1, Apache-2.0, BSD-4-Clause, BSD-4-Clause-UC, BSD-4.3TAHOE, ECL-2.0, FTL, IJG, LicenseRef-scancode-bsla-no-advert, Minpack, OpenSSL, PHP-3.01, Python-2.0, zlib-acknowledgement, XFree86-1.1",
25+
"name": "LGPL-2.1-or-later",
26+
"osadl_updated": "2025-02-10T14:26:00+0000",
27+
"patent_hints": "yes",
28+
"source": "file_header",
29+
"url": "https://spdx.org/licenses/LGPL-2.1-or-later.html"
30+
}
31+
],
32+
"lines": "16-24",
33+
"matched": "15%",
34+
"oss_lines": "34-42",
35+
"purl": [
36+
"pkg:sourceforge/check"
37+
],
38+
"release_date": "2002-03-02",
39+
"server": {
40+
"kb_version": {
41+
"daily": "25.05.14",
42+
"monthly": "25.04"
43+
},
44+
"version": "5.4.10"
45+
},
46+
"source_hash": "74c49597c2934e08b2ce8797f4aa7454",
47+
"status": "pending",
48+
"url": "https://sourceforge.net/projects/check",
49+
"url_hash": "d81953e1dca4c498140c44f5d6fa92d6",
50+
"vendor": "check",
51+
"version": "0.8.1"
52+
}
53+
]
54+
}

0 commit comments

Comments
 (0)