diff --git a/buildSrc/src/main/kotlin/LicenseUtils.kt b/buildSrc/src/main/kotlin/LicenseUtils.kt index 26b99631b95f6..b9561321ad687 100644 --- a/buildSrc/src/main/kotlin/LicenseUtils.kt +++ b/buildSrc/src/main/kotlin/LicenseUtils.kt @@ -34,6 +34,7 @@ object CopyrightableFiles { "plugins/reporters/asciidoc/src/main/resources/pdf-theme/pdf-theme.yml", "plugins/reporters/asciidoc/src/main/resources/templates/freemarker_implicit.ftl", "plugins/reporters/fossid/src/main/resources/templates/freemarker_implicit.ftl", + "plugins/reporters/scanoss/src/main/resources/templates/freemarker_implicit.ftl", "plugins/reporters/freemarker/src/main/resources/templates/freemarker_implicit.ftl", "plugins/reporters/static-html/src/main/resources/prismjs/", "plugins/reporters/web-app-template/yarn.lock", diff --git a/integrations/completions/ort-completion.fish b/integrations/completions/ort-completion.fish index 67aba1cc1859c..5e565980cf0f0 100644 --- a/integrations/completions/ort-completion.fish +++ b/integrations/completions/ort-completion.fish @@ -149,7 +149,7 @@ complete -c ort -f -n __fish_use_subcommand -a report -d 'Present Analyzer, Scan ## Options for report complete -c ort -n "__fish_seen_subcommand_from report" -l ort-file -s i -r -F -d 'The ORT result file to use.' complete -c ort -n "__fish_seen_subcommand_from report" -l output-dir -s o -r -F -d 'The output directory to store the generated reports in.' -complete -c ort -n "__fish_seen_subcommand_from report" -l report-formats -s f -r -d 'A comma-separated list of report formats to generate, any of [AOSD2.0, AOSD2.1, CtrlXAutomation, CycloneDX, DocBookTemplate, EvaluatedModel, FossID, FossIdSnippet, HtmlTemplate, ManPageTemplate, Opossum, PdfTemplate, PlainTextTemplate, SpdxDocument, StaticHTML, TrustSource, WebApp].' +complete -c ort -n "__fish_seen_subcommand_from report" -l report-formats -s f -r -d 'A comma-separated list of report formats to generate, any of [AOSD2.0, AOSD2.1, CtrlXAutomation, CycloneDX, DocBookTemplate, EvaluatedModel, FossID, FossIdSnippet, HtmlTemplate, ManPageTemplate, Opossum, PdfTemplate, PlainTextTemplate, ScanossSnippet, SpdxDocument, StaticHTML, TrustSource, WebApp].' complete -c ort -n "__fish_seen_subcommand_from report" -l copyright-garbage-file -r -F -d 'A file containing copyright statements which are marked as garbage. This can make the output inconsistent with the evaluator output but is useful when testing copyright garbage.' complete -c ort -n "__fish_seen_subcommand_from report" -l custom-license-texts-dir -r -F -d 'A directory which maps custom license IDs to license texts. It should contain one text file per license with the license ID as the filename. A custom license text is used only if its ID has a \'LicenseRef-\' prefix and if the respective license text is not known by ORT.' complete -c ort -n "__fish_seen_subcommand_from report" -l how-to-fix-text-provider-script -r -F -d 'The path to a Kotlin script which returns an instance of a \'HowToFixTextProvider\'. That provider injects how-to-fix texts in Markdown format for ORT issues.' diff --git a/plugins/reporters/scanoss/build.gradle.kts b/plugins/reporters/scanoss/build.gradle.kts new file mode 100644 index 0000000000000..7feaf0d15d6da --- /dev/null +++ b/plugins/reporters/scanoss/build.gradle.kts @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +plugins { + // Apply precompiled plugins. + id("ort-plugin-conventions") +} + +dependencies { + api(projects.reporter) + + ksp(projects.reporter) + + implementation(projects.model) + implementation(projects.plugins.reporters.asciidocReporter) + implementation(projects.plugins.reporters.freemarkerReporter) + implementation(projects.utils.commonUtils) + implementation(projects.utils.ortUtils) + + implementation(libs.kotlinx.coroutines) + + testImplementation(libs.mockk) +} diff --git a/plugins/reporters/scanoss/src/main/kotlin/ScanossSnippetReporter.kt b/plugins/reporters/scanoss/src/main/kotlin/ScanossSnippetReporter.kt new file mode 100644 index 0000000000000..0f482fe3f9acc --- /dev/null +++ b/plugins/reporters/scanoss/src/main/kotlin/ScanossSnippetReporter.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.reporters.scanoss + +import java.io.File + +import org.ossreviewtoolkit.plugins.api.OrtPlugin +import org.ossreviewtoolkit.plugins.api.PluginDescriptor +import org.ossreviewtoolkit.plugins.reporters.asciidoc.AsciiDocTemplateReporterConfig +import org.ossreviewtoolkit.plugins.reporters.asciidoc.HtmlTemplateReporter +import org.ossreviewtoolkit.reporter.Reporter +import org.ossreviewtoolkit.reporter.ReporterFactory +import org.ossreviewtoolkit.reporter.ReporterInput + +@OrtPlugin( + displayName = "SCANOSS Snippet Reporter", + description = "Generates a detailed report of the SCANOSS snippet findings.", + factory = ReporterFactory::class +) +class ScanossSnippetReporter(override val descriptor: PluginDescriptor = ScanossSnippetReporterFactory.descriptor) : + Reporter by delegateReporter { + companion object { + private val delegateReporter = HtmlTemplateReporter( + ScanossSnippetReporterFactory.descriptor, + AsciiDocTemplateReporterConfig(templateIds = listOf("scanoss_snippet"), templatePaths = null) + ) + } + + override fun generateReport(input: ReporterInput, outputDir: File): List> { + val hasScanossResults = input.ortResult.scanner?.scanResults?.any { it.scanner.name == "SCANOSS" } == true + require(hasScanossResults) { "No SCANOSS scan results have been found." } + + return delegateReporter.generateReport(input, outputDir) + } +} diff --git a/plugins/reporters/scanoss/src/main/resources/templates/.gitattributes b/plugins/reporters/scanoss/src/main/resources/templates/.gitattributes new file mode 100644 index 0000000000000..abca6329e9e97 --- /dev/null +++ b/plugins/reporters/scanoss/src/main/resources/templates/.gitattributes @@ -0,0 +1,2 @@ +# Use Unix line endings for Freemarker templates for consistency across platforms. +**/*.ftl text eol=lf diff --git a/plugins/reporters/scanoss/src/main/resources/templates/asciidoc/scanoss_snippet.ftl b/plugins/reporters/scanoss/src/main/resources/templates/asciidoc/scanoss_snippet.ftl new file mode 100644 index 0000000000000..cc580d9d8bb0c --- /dev/null +++ b/plugins/reporters/scanoss/src/main/resources/templates/asciidoc/scanoss_snippet.ftl @@ -0,0 +1,142 @@ +[#-- + Copyright (C) 2025 The ORT Project Authors (see ) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 + License-Filename: LICENSE +--] + +:publisher: OSS Review Toolkit +[#assign now = .now] +:revdate: ${now?date?iso_local} + +:title-page: +:sectnums: +:toc: + += SCANOSS Snippets +List of all the provenances with their files and snippets. +[#list ortResult.scanner.scanResults as scanResult] + +[#assign snippetsLimitIssue = helper.getSnippetsLimitIssue()] + +[#if snippetsLimitIssue?has_content] +[WARNING] +==== +${snippetsLimitIssue} +==== +[/#if] + +[#if scanResult.provenance.vcsInfo??] + [#assign url = scanResult.provenance.vcsInfo.url] +[#else] + [#assign url = scanResult.provenance.sourceArtifact.url] +[/#if] +== Provenance '${url}' + +[#assign summary = scanResult.summary] + +Scan start time : ${summary.startTime} + +End time : ${summary.startTime} + +[#if scanResult.provenance.vcsInfo??] + [#assign gitRepoUrl = url] + [#assign gitRevision = scanResult.provenance.vcsInfo.revision] + Git repo URL: ${gitRepoUrl} + + Git revision: ${gitRevision} + + [#if gitRepoUrl?contains("github.com")] + [#assign githubBaseURL = '${gitRepoUrl?remove_ending(".git")}/blob/${gitRevision}'] + [/#if] +[/#if] + +[#list helper.groupSnippetsByFile(summary.snippetFindings) as filePath, snippetFindings ] + +[#if gitRepoUrl?? && gitRepoUrl?contains("github.com")] + [#assign localFileURL = '${githubBaseURL}/${filePath}[${filePath}]'] +[#else] + [#assign localFileURL = "${filePath}"] +[/#if] +[#assign licenses = helper.collectLicenses(snippetFindings)] + +*${localFileURL}* + +License(s): +[#list licenses as license] + ${license}[#sep], +[/#list] + +[#list helper.groupSnippetsBySourceLines(snippetFindings) as sourceLocation, snippetFinding] +[#assign snippetCount = snippetFinding.snippets?size] + +[width=100%] +[cols="1,3,4,1,2"] +|=== +| Source Location | pURL | License | Score | Release Date + +.${snippetCount*2}+| +Partial match + +${sourceLocation.startLine?c}-${sourceLocation.endLine?c} + + +[#list snippetFinding.snippets as snippet ] + +| ${snippet.purl!""} +| ${snippet.license!""} +| ${snippet.score!""} +| ${snippet.additionalData['release_date']} + +4+a| +.Create a snippet choice for this snippet or mark it as false positive +[%collapsible] +==== +Add the following lines to the *.ort.yml* file. + +To **choose** this snippet: +[source,yaml] +-- +snippet_choices: +- provenance: + url: "${scanResult.provenance.vcsInfo.url}" + choices: + - given: + source_location: + path: "${filePath}" + start_line: ${snippetFinding.sourceLocation.startLine?c} + end_line: ${snippetFinding.sourceLocation.endLine?c} + choice: + purl: "${snippet.purl!""}" + reason: "ORIGINAL_FINDING" + comment: "Explain why this snippet choice was made" +-- +Or to mark this location has having ONLY **false positives snippets**: +[source,yaml] +-- +snippet_choices: +- provenance: + url: "${scanResult.provenance.vcsInfo.url}" + choices: + - given: + source_location: + path: "${filePath}" + start_line: ${snippetFinding.sourceLocation.startLine?c} + end_line: ${snippetFinding.sourceLocation.endLine?c} + choice: + reason: "NO_RELEVANT_FINDING" + comment: "Explain why this location has only false positives snippets" +-- +==== +[/#list] +|=== +[/#list] +[/#list] +[/#list] diff --git a/plugins/reporters/scanoss/src/main/resources/templates/freemarker_implicit.ftl b/plugins/reporters/scanoss/src/main/resources/templates/freemarker_implicit.ftl new file mode 100644 index 0000000000000..a409081912143 --- /dev/null +++ b/plugins/reporters/scanoss/src/main/resources/templates/freemarker_implicit.ftl @@ -0,0 +1,10 @@ +[#ftl] +[#-- @implicitly included --] + +[#-- @ftlvariable name="projects" type="kotlin.collections.Set" --] +[#-- @ftlvariable name="packages" type="kotlin.collections.Set" --] +[#-- @ftlvariable name="ortResult" type="org.ossreviewtoolkit.model.OrtResult" --] +[#-- @ftlvariable name="licenseTextProvider" type="org.ossreviewtoolkit.reporter.LicenseTextProvider" --] +[#-- @ftlvariable name="LicenseView" type="org.ossreviewtoolkit.model.licenses.LicenseView" --] +[#-- @ftlvariable name="helper" type="org.ossreviewtoolkit.plugins.reporters.freemarker.FreemarkerTemplateProcessor.TemplateHelper" --] +[#-- @ftlvariable name="projectsAsPackages" type="kotlin.collections.Set" --] diff --git a/plugins/scanners/scanoss/src/main/kotlin/ScanOssResultParser.kt b/plugins/scanners/scanoss/src/main/kotlin/ScanOssResultParser.kt index 9522a0b3e0016..d81250cc0df1b 100644 --- a/plugins/scanners/scanoss/src/main/kotlin/ScanOssResultParser.kt +++ b/plugins/scanners/scanoss/src/main/kotlin/ScanOssResultParser.kt @@ -18,13 +18,17 @@ */ package org.ossreviewtoolkit.plugins.scanners.scanoss - +import com.scanoss.dto.LicenseDetails import com.scanoss.dto.ScanFileDetails import com.scanoss.dto.ScanFileResult import com.scanoss.dto.enums.MatchType +import com.scanoss.dto.enums.StatusType +import java.lang.invoke.MethodHandles import java.time.Instant +import org.apache.logging.log4j.kotlin.loggerOf + import org.ossreviewtoolkit.downloader.VcsHost import org.ossreviewtoolkit.model.CopyrightFinding import org.ossreviewtoolkit.model.LicenseFinding @@ -36,7 +40,8 @@ import org.ossreviewtoolkit.model.TextLocation import org.ossreviewtoolkit.utils.spdx.SpdxConstants import org.ossreviewtoolkit.utils.spdx.SpdxExpression import org.ossreviewtoolkit.utils.spdx.SpdxLicenseIdExpression -import org.ossreviewtoolkit.utils.spdx.toExpression + +private val logger = loggerOf(MethodHandles.lookup().lookupClass()) /** * Generate a summary from the given SCANOSS [result], using [startTime], [endTime] as metadata. This variant can be @@ -56,16 +61,29 @@ internal fun generateSummary(startTime: Instant, endTime: Instant, results: List } MatchType.snippet -> { - val file = requireNotNull(details.file) - val lines = requireNotNull(details.lines) - val sourceLocations = convertLines(file, lines) - val snippets = getSnippets(details) - - snippets.forEach { snippet -> - sourceLocations.forEach { sourceLocation -> - // TODO: Aggregate the snippet by source file location. - snippetFindings += SnippetFinding(sourceLocation, setOf(snippet)) + val file = requireNotNull(result.filePath) + if (details.status == StatusType.pending) { + val lines = requireNotNull(details.lines) + val sourceLocations = convertLines(file, lines) + val snippets = getSnippets(details) + + // The number of snippets should match the number of source locations. + if (sourceLocations.size != snippets.size) { + logger.warn { + "Unexpected mismatch in '$file': " + + "${sourceLocations.size} source locations vs ${snippets.size} snippets. " + + "This indicates a potential issue with line range conversion." + } } + + // Associate each source location with its corresponding snippet. + sourceLocations.zip(snippets).forEach { (location, snippet) -> + snippetFindings += SnippetFinding(location, setOf(snippet)) + } + } else { + logger.warn { "File '$file' is identified, not including on snippet findings" } + licenseFindings += getLicenseFindings(details) + copyrightFindings += getCopyrightFindings(details) } } @@ -134,19 +152,18 @@ private fun getCopyrightFindings(details: ScanFileDetails): List { +private fun getSnippets(details: ScanFileDetails): List { val matched = requireNotNull(details.matched) val fileUrl = requireNotNull(details.fileUrl) val ossLines = requireNotNull(details.ossLines) val url = requireNotNull(details.url) val purls = requireNotNull(details.purls) - val licenses = details.licenseDetails.orEmpty().mapTo(mutableSetOf()) { license -> - SpdxExpression.parse(license.name) - } + val license = getUniqueLicenseExpression(details.licenseDetails.toList()) val score = matched.substringBeforeLast("%").toFloat() val locations = convertLines(fileUrl, ossLines) @@ -154,14 +171,15 @@ private fun getSnippets(details: ScanFileDetails): Set { val vcsInfo = VcsHost.parseUrl(url.takeUnless { it == "none" }.orEmpty()) val provenance = RepositoryProvenance(vcsInfo, ".") - return buildSet { - purls.forEach { purl -> - locations.forEach { snippetLocation -> - val license = licenses.toExpression()?.sorted() ?: SpdxLicenseIdExpression(SpdxConstants.NOASSERTION) + // Store all PURLs in additionalData to preserve the complete information. + val additionalData = mapOf( + "release_date" to details.releaseDate, + "all_purls" to purls.joinToString(" ") + ) - add(Snippet(score, snippetLocation, provenance, purl, license)) - } - } + // Create one snippet per location, using the first PURL as the primary identifier. + return locations.map { snippetLocation -> + Snippet(score, snippetLocation, provenance, purls.firstOrNull().orEmpty(), license, additionalData) } } @@ -178,3 +196,14 @@ private fun convertLines(file: String, lineRanges: String): List = else -> throw IllegalArgumentException("Unsupported line range '$lineRange'.") } } + +fun getUniqueLicenseExpression(licensesDetails: List): SpdxExpression { + if (licensesDetails.isEmpty()) { + return SpdxLicenseIdExpression(SpdxConstants.NOASSERTION) + } + + return licensesDetails + .map { license -> SpdxExpression.parse(license.name) } + .reduce { acc, expr -> acc and expr } + .simplify() +} diff --git a/plugins/scanners/scanoss/src/test/assets/scanoss-multiple-purls.json b/plugins/scanners/scanoss/src/test/assets/scanoss-multiple-purls.json new file mode 100644 index 0000000000000..7e02115508b8a --- /dev/null +++ b/plugins/scanners/scanoss/src/test/assets/scanoss-multiple-purls.json @@ -0,0 +1,60 @@ +{ + "hung_task.c": [ + { + "component": "proton_bluecross", + "file": "kernel/hung_task.c", + "file_hash": "581734935cfbe570d280a1265aaa2a6b", + "file_url": "https://api.scanoss.com/file_contents/581734935cfbe570d280a1265aaa2a6b", + "id": "snippet", + "latest": "17", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "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", + "name": "GPL-2.0-only", + "osadl_updated": "2025-02-10T14:26:00+0000", + "patent_hints": "yes", + "source": "scancode", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + }, + { + "name": "GPL-2.0-only WITH Linux-syscall-note", + "source": "scancode", + "url": "https://spdx.org/licenses/GPL-2.0-only WITH Linux-syscall-note.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/GPL-2.0-only.txt", + "copyleft": "yes", + "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", + "name": "GPL-2.0-only", + "osadl_updated": "2025-02-10T14:26:00+0000", + "patent_hints": "yes", + "source": "scancode", + "url": "https://spdx.org/licenses/GPL-2.0-only.html" + } + ], + "lines": "12-150,540-561", + "matched": "35%", + "oss_lines": "10-148,86-107", + "purl": [ + "pkg:github/kdrag0n/proton_bluecross", + "pkg:github/fake/fake_repository" + ], + "release_date": "2019-02-21", + "server": { + "kb_version": { + "daily": "25.03.27", + "monthly": "25.03" + }, + "version": "5.4.10" + }, + "source_hash": "45dd1e50621a8a32f88fbe0251a470ab", + "status": "pending", + "url": "https://github.com/kdrag0n/proton_bluecross", + "url_hash": "a9c1c67f0930dc42dbd40c29e565bcdd", + "vendor": "kdrag0n", + "version": "15" + } + ] +} diff --git a/plugins/scanners/scanoss/src/test/kotlin/ScanOssResultParserTest.kt b/plugins/scanners/scanoss/src/test/kotlin/ScanOssResultParserTest.kt index 3fbef55245ddf..4d3a06d001946 100644 --- a/plugins/scanners/scanoss/src/test/kotlin/ScanOssResultParserTest.kt +++ b/plugins/scanners/scanoss/src/test/kotlin/ScanOssResultParserTest.kt @@ -19,6 +19,7 @@ package org.ossreviewtoolkit.plugins.scanners.scanoss +import com.scanoss.dto.LicenseDetails import com.scanoss.utils.JsonUtils import io.kotest.core.spec.style.WordSpec @@ -27,6 +28,7 @@ import io.kotest.matchers.collections.containExactlyInAnyOrder import io.kotest.matchers.collections.haveSize import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.should +import io.kotest.matchers.shouldBe import java.io.File import java.time.Instant @@ -39,9 +41,44 @@ import org.ossreviewtoolkit.model.SnippetFinding import org.ossreviewtoolkit.model.TextLocation import org.ossreviewtoolkit.model.VcsInfo import org.ossreviewtoolkit.model.VcsType +import org.ossreviewtoolkit.utils.spdx.SpdxConstants import org.ossreviewtoolkit.utils.spdx.SpdxExpression +import org.ossreviewtoolkit.utils.spdx.SpdxLicenseIdExpression class ScanOssResultParserTest : WordSpec({ + "getUniqueLicenseDetails()" should { + "deduplicate complex license expressions" { + val uniqueLicenses = getUniqueLicenseExpression( + listOf( + LicenseDetails.builder().name("MIT").build(), + LicenseDetails.builder().name("MIT").build(), + LicenseDetails.builder().name("GPL-2.0-only").build(), + LicenseDetails.builder().name("GPL-2.0-only WITH Linux-syscall-note").build(), + LicenseDetails.builder().name("GPL-2.0-only AND MIT").build() + ) + ) + + val decomposed = uniqueLicenses.decompose().toList() + + val expressionStrings = decomposed.map { it.toString() } + + // Check that each license appears exactly once + expressionStrings.count { it == "MIT" } shouldBe 1 + expressionStrings.count { it == "GPL-2.0-only" } shouldBe 1 + expressionStrings.count { it == "GPL-2.0-only WITH Linux-syscall-note" } shouldBe 1 + + // Ensure no unexpected elements + expressionStrings.size shouldBe 3 + } + + "handle empty license list" { + val emptyLicenses = getUniqueLicenseExpression(listOf()) + + // Verify empty license list returns NOASSERTION + emptyLicenses shouldBe SpdxLicenseIdExpression(SpdxConstants.NOASSERTION) + } + } + "generateSummary()" should { "properly summarize JUnit 4.12 findings" { val results = File("src/test/assets/scanoss-junit-4.12.json").readText().let { @@ -125,11 +162,52 @@ class ScanOssResultParserTest : WordSpec({ "." ), "pkg:github/vdurmont/semver4j", - SpdxExpression.parse("CC-BY-SA-2.0") + SpdxExpression.parse("CC-BY-SA-2.0"), + additionalData = mapOf( + "release_date" to "2019-09-13", + "all_purls" to "pkg:github/vdurmont/semver4j" + ) ) ) ) ) } + + "should handle multiple PURLs by selecting first as primary and preserving all in metadata" { + val results = File("src/test/assets/scanoss-multiple-purls.json").readText().let { + JsonUtils.toScanFileResultsFromObject(JsonUtils.toJsonObject(it)) + } + + val time = Instant.now() + val summary = generateSummary(time, time, results) + + // Should have one finding per source location, not per PURL. + summary.snippetFindings should haveSize(2) + + with(summary.snippetFindings.first()) { + // Check source location (local file). + sourceLocation shouldBe TextLocation("hung_task.c", 12, 150) + + // Should use first PURL as primary identifier. + snippets should haveSize(1) + snippets.first().purl shouldBe "pkg:github/kdrag0n/proton_bluecross" + + // Should preserve all PURLs in additionalData. + snippets.first().additionalData["all_purls"] shouldBe + "pkg:github/kdrag0n/proton_bluecross pkg:github/fake/fake_repository" + + // Check OSS location. + snippets.first().location shouldBe + TextLocation("https://api.scanoss.com/file_contents/581734935cfbe570d280a1265aaa2a6b", 10, 148) + } + + // Verify same behavior for second snippet. + with(summary.snippetFindings.last()) { + sourceLocation shouldBe TextLocation("hung_task.c", 540, 561) + snippets.first().purl shouldBe "pkg:github/kdrag0n/proton_bluecross" + snippets.first().location shouldBe + TextLocation("https://api.scanoss.com/file_contents/581734935cfbe570d280a1265aaa2a6b", 86, 107) + } + } } }) diff --git a/plugins/scanners/scanoss/src/test/kotlin/ScanOssScannerDirectoryTest.kt b/plugins/scanners/scanoss/src/test/kotlin/ScanOssScannerDirectoryTest.kt index 4c910465a8129..3836806e1a84d 100644 --- a/plugins/scanners/scanoss/src/test/kotlin/ScanOssScannerDirectoryTest.kt +++ b/plugins/scanners/scanoss/src/test/kotlin/ScanOssScannerDirectoryTest.kt @@ -111,7 +111,12 @@ class ScanOssScannerDirectoryTest : StringSpec({ VcsInfo(VcsType.GIT, "https://github.com/scanoss/ort.git", ""), "." ), "pkg:github/scanoss/ort", - SpdxExpression.parse("Apache-2.0") + SpdxExpression.parse("Apache-2.0"), + additionalData = mapOf( + "release_date" to "2021-03-18", + "all_purls" to "pkg:github/scanoss/ort" + ) + ) ) )