diff --git a/scanner/src/main/kotlin/scanners/scanoss/ScanOssResultParser.kt b/scanner/src/main/kotlin/scanners/scanoss/ScanOssResultParser.kt index d52117e8d13e0..ba57cf1b0bc47 100644 --- a/scanner/src/main/kotlin/scanners/scanoss/ScanOssResultParser.kt +++ b/scanner/src/main/kotlin/scanners/scanoss/ScanOssResultParser.kt @@ -23,11 +23,17 @@ import java.io.File import java.time.Instant import org.ossreviewtoolkit.clients.scanoss.FullScanResponse +import org.ossreviewtoolkit.clients.scanoss.model.IdentificationType import org.ossreviewtoolkit.clients.scanoss.model.ScanResponse import org.ossreviewtoolkit.model.CopyrightFinding import org.ossreviewtoolkit.model.LicenseFinding +import org.ossreviewtoolkit.model.RepositoryProvenance import org.ossreviewtoolkit.model.ScanSummary import org.ossreviewtoolkit.model.TextLocation +import org.ossreviewtoolkit.model.VcsInfo +import org.ossreviewtoolkit.model.VcsType +import org.ossreviewtoolkit.model.utils.Snippet +import org.ossreviewtoolkit.model.utils.SnippetFinding import org.ossreviewtoolkit.utils.spdx.SpdxConstants import org.ossreviewtoolkit.utils.spdx.SpdxExpression import org.ossreviewtoolkit.utils.spdx.calculatePackageVerificationCode @@ -64,11 +70,25 @@ internal fun generateSummary( ): ScanSummary { val licenseFindings = mutableListOf() val copyrightFindings = mutableListOf() + val snippetFindings = mutableSetOf() result.forEach { (_, scanResponses) -> scanResponses.forEach { scanResponse -> - licenseFindings += getLicenseFindings(scanResponse, detectedLicenseMapping) - copyrightFindings += getCopyrightFindings(scanResponse) + if (scanResponse.id == IdentificationType.FILE) { + licenseFindings += getLicenseFindings(scanResponse, detectedLicenseMapping) + copyrightFindings += getCopyrightFindings(scanResponse) + } + + if (scanResponse.id == IdentificationType.SNIPPET) { + val file = requireNotNull(scanResponse.file) + val lines = requireNotNull(scanResponse.lines) + val sourceLocation = convertLines(file, lines) + val snippets = getSnippets(scanResponse) + + snippets.forEach { + snippetFindings += SnippetFinding(sourceLocation, it) + } + } } } @@ -78,6 +98,7 @@ internal fun generateSummary( packageVerificationCode = verificationCode, licenseFindings = licenseFindings.toSortedSet(), copyrightFindings = copyrightFindings.toSortedSet(), + snippetFindings = snippetFindings.toSortedSet(), issues = emptyList() ) } @@ -131,3 +152,46 @@ private fun getCopyrightFindings(scanResponse: ScanResponse): List { + val matched = requireNotNull(scanResponse.matched) + val fileUrl = requireNotNull(scanResponse.fileUrl) + val ossLines = requireNotNull(scanResponse.ossLines) + val url = requireNotNull(scanResponse.url) + val purls = requireNotNull(scanResponse.purl) + + val licenses = scanResponse.licenses.map { license -> + SpdxExpression.parse(license.name) + }.toSet() + + val score = matched.substringBeforeLast("%").toFloat() + val snippetLocation = convertLines(fileUrl, ossLines) + // TODO: No resolved revision is available. Should a ArtifactProvenance be created instead ? + val snippetProvenance = RepositoryProvenance(VcsInfo(VcsType.UNKNOWN, url, ""), ".") + + return purls.map { + Snippet( + score, + snippetLocation, + snippetProvenance, + it, + licenses.distinct().reduce(SpdxExpression::and).sorted() + ) + }.toSortedSet() +} + +/** + * Split a [lineRange] returned by ScanOSS such as 1-321 into a [TextLocation] for the given [file]. + */ +private fun convertLines(file: String, lineRange: String): TextLocation { + val splitLines = lineRange.split("-") + return if (splitLines.size == 2) { + TextLocation(file, splitLines.first().toInt(), splitLines.last().toInt()) + } else { + TextLocation(file, splitLines.first().toInt()) + } +} diff --git a/scanner/src/test/assets/scanoss-semver4j-3.1.0-with-snippet.json b/scanner/src/test/assets/scanoss-semver4j-3.1.0-with-snippet.json new file mode 100644 index 0000000000000..a553470bd4782 --- /dev/null +++ b/scanner/src/test/assets/scanoss-semver4j-3.1.0-with-snippet.json @@ -0,0 +1,415 @@ +{ + "src/main/java/com/vdurmont/semver4j/Range.java": [ + { + "component": "semver4j", + "file": "com/vdurmont/semver4j/Range.java", + "file_hash": "8b917844b6bb13a7a377d091cac9e231", + "file_url": "https://osskb.org/api/file_contents/8b917844b6bb13a7a377d091cac9e231", + "id": "file", + "latest": "2.2.0-graylog.1", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/Apache-2.0.txt", + "copyleft": "no", + "name": "Apache-2.0", + "osadl_updated": "2023-03-26T02:11:00+00:00", + "patent_hints": "yes", + "source": "license_file", + "url": "https://spdx.org/licenses/Apache-2.0.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/BSD-2-Clause.txt", + "copyleft": "no", + "name": "BSD-2-Clause", + "osadl_updated": "2023-03-26T02:11:00+00:00", + "patent_hints": "no", + "source": "license_file", + "url": "https://spdx.org/licenses/BSD-2-Clause.html" + }, + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/EPL-2.0.txt", + "copyleft": "yes", + "name": "EPL-2.0", + "osadl_updated": "2023-03-26T02:11:00+00:00", + "patent_hints": "yes", + "source": "license_file", + "url": "https://spdx.org/licenses/EPL-2.0.html" + }, + { + "name": "SSPL", + "source": "license_file", + "url": "https://spdx.org/licenses/SSPL.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:maven/org.graylog.repackaged/semver4j", + "pkg:maven/org.graylog2/graylog2-server", + "pkg:github/graylog2/graylog2-server", + "pkg:maven/org.graylog/graylog-parent", + "pkg:maven/org.graylog/graylog-project-parent", + "pkg:maven/org.graylog.repackaged/os-platform-finder", + "pkg:maven/org.graylog.telemetry/graylog-telemetry-plugin", + "pkg:maven/org.graylog.plugins/graylog-plugin-parent", + "pkg:maven/org.graylog.shaded/kafka09", + "pkg:maven/org.graylog.shaded/elasticsearch5" + ], + "release_date": "2018-07-04", + "server": { + "kb_version": { + "daily": "23.03.19", + "monthly": "23.03" + }, + "version": "5.2.5" + }, + "source_hash": "8b917844b6bb13a7a377d091cac9e231", + "status": "pending", + "url": "https://mvnrepository.com/artifact/org.graylog.repackaged/semver4j", + "url_hash": "9b0606cb3068e838edb89aeb2158d3be", + "vendor": "org.graylog.repackaged", + "version": "2.2.0-graylog.1" + } + ], + "src/main/java/com/vdurmont/semver4j/Requirement.java":[ + { + "id": "snippet", + "lines": "1-710", + "oss_lines": "1-710", + "matched": "98%", + "file_hash": "6ff2427335b985212c9b79dfa795799f", + "source_hash": "bd4bff27f540f4f2c9de012acc4b48a3", + "file_url": "https://osskb.org/api/file_contents/6ff2427335b985212c9b79dfa795799f", + "purl": [ + "pkg:github/vdurmont/semver4j" + ], + "vendor": "vdurmont", + "component": "semver4j", + "version": "3.1.0", + "latest": "3.1.0", + "url": "https://github.com/vdurmont/semver4j", + "status": "pending", + "release_date": "2019-09-13", + "file": "src/main/java/com/vdurmont/semver4j/Requirement.java", + "url_hash": "b92cd7e4d588747588d1b5fe1f8c664c", + "licenses": [ + { + "name": "CC-BY-SA-2.0", + "patent_hints": "no", + "copyleft": "no", + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/MIT.txt", + "osadl_updated": "2023-03-26T02:11:00+00:00", + "source": "component_declared", + "url": "https://scancode-licensedb.aboutcode.org/cc-by-sa-2.0.LICENSE" + } + ], + "server": { + "version": "5.2.5", + "kb_version": { + "monthly": "23.03", + "daily": "23.03.19" + } + } + } + ], + "src/main/java/com/vdurmont/semver4j/Semver.java": [ + { + "component": "semver4j", + "file": "src/main/java/com/vdurmont/semver4j/Semver.java", + "file_hash": "a51addd1e051f8ff4feb0fc879726e2c", + "file_url": "https://osskb.org/api/file_contents/a51addd1e051f8ff4feb0fc879726e2c", + "id": "file", + "latest": "3.1.0", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/MIT.txt", + "copyleft": "no", + "name": "MIT", + "osadl_updated": "2023-03-26T02:11:00+00:00", + "patent_hints": "no", + "source": "component_declared", + "url": "https://spdx.org/licenses/MIT.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/vdurmont/semver4j" + ], + "release_date": "2019-09-13", + "server": { + "kb_version": { + "daily": "23.03.19", + "monthly": "23.03" + }, + "version": "5.2.5" + }, + "source_hash": "a51addd1e051f8ff4feb0fc879726e2c", + "status": "pending", + "url": "https://github.com/vdurmont/semver4j", + "url_hash": "b92cd7e4d588747588d1b5fe1f8c664c", + "vendor": "vdurmont", + "version": "3.1.0" + } + ], + "src/main/java/com/vdurmont/semver4j/SemverException.java": [ + { + "id": "none", + "server": { + "kb_version": { + "daily": "23.03.19", + "monthly": "23.03" + }, + "version": "5.2.5" + } + } + ], + "src/main/java/com/vdurmont/semver4j/Tokenizer.java": [ + { + "component": "semver4j", + "file": "src/main/java/com/vdurmont/semver4j/Tokenizer.java", + "file_hash": "72189e2d96994bde6483571eb6bf1825", + "file_url": "https://osskb.org/api/file_contents/72189e2d96994bde6483571eb6bf1825", + "id": "file", + "latest": "3.1.0", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/MIT.txt", + "copyleft": "no", + "name": "MIT", + "osadl_updated": "2023-03-26T02:11:00+00:00", + "patent_hints": "no", + "source": "component_declared", + "url": "https://spdx.org/licenses/MIT.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/vdurmont/semver4j" + ], + "release_date": "2019-09-13", + "server": { + "kb_version": { + "daily": "23.03.19", + "monthly": "23.03" + }, + "version": "5.2.5" + }, + "source_hash": "72189e2d96994bde6483571eb6bf1825", + "status": "pending", + "url": "https://github.com/vdurmont/semver4j", + "url_hash": "b92cd7e4d588747588d1b5fe1f8c664c", + "vendor": "vdurmont", + "version": "3.1.0" + } + ], + "src/test/java/com/vdurmont/semver4j/NpmSemverTest.java": [ + { + "component": "semver4j", + "file": "src/test/java/com/vdurmont/semver4j/NpmSemverTest.java", + "file_hash": "8fa5ef7baab5071e49b2393905231420", + "file_url": "https://osskb.org/api/file_contents/8fa5ef7baab5071e49b2393905231420", + "id": "file", + "latest": "3.1.0", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/MIT.txt", + "copyleft": "no", + "name": "MIT", + "osadl_updated": "2023-03-26T02:11:00+00:00", + "patent_hints": "no", + "source": "component_declared", + "url": "https://spdx.org/licenses/MIT.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/vdurmont/semver4j" + ], + "release_date": "2019-09-13", + "server": { + "kb_version": { + "daily": "23.03.19", + "monthly": "23.03" + }, + "version": "5.2.5" + }, + "source_hash": "8fa5ef7baab5071e49b2393905231420", + "status": "pending", + "url": "https://github.com/vdurmont/semver4j", + "url_hash": "b92cd7e4d588747588d1b5fe1f8c664c", + "vendor": "vdurmont", + "version": "3.1.0" + } + ], + "src/test/java/com/vdurmont/semver4j/RangeTest.java": [ + { + "component": "semver4j", + "file": "src/test/java/com/vdurmont/semver4j/RangeTest.java", + "file_hash": "7069133e87c482fe935a7a54ca5b5a5d", + "file_url": "https://osskb.org/api/file_contents/7069133e87c482fe935a7a54ca5b5a5d", + "id": "file", + "latest": "3.1.0", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/MIT.txt", + "copyleft": "no", + "name": "MIT", + "osadl_updated": "2023-03-26T02:11:00+00:00", + "patent_hints": "no", + "source": "component_declared", + "url": "https://spdx.org/licenses/MIT.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/vdurmont/semver4j" + ], + "release_date": "2019-08-16", + "server": { + "kb_version": { + "daily": "23.03.19", + "monthly": "23.03" + }, + "version": "5.2.5" + }, + "source_hash": "7069133e87c482fe935a7a54ca5b5a5d", + "status": "pending", + "url": "https://github.com/vdurmont/semver4j", + "url_hash": "7cc91d1fce4275d9ef74fb5ae1bd4f63", + "vendor": "vdurmont", + "version": "3.0.0" + } + ], + "src/test/java/com/vdurmont/semver4j/RequirementTest.java": [ + { + "component": "semver4j", + "file": "src/test/java/com/vdurmont/semver4j/RequirementTest.java", + "file_hash": "5e9fbaa3bde70a3a5a077fe6d65e36de", + "file_url": "https://osskb.org/api/file_contents/5e9fbaa3bde70a3a5a077fe6d65e36de", + "id": "file", + "latest": "3.1.0", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/MIT.txt", + "copyleft": "no", + "name": "MIT", + "osadl_updated": "2023-03-26T02:11:00+00:00", + "patent_hints": "no", + "source": "component_declared", + "url": "https://spdx.org/licenses/MIT.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/vdurmont/semver4j" + ], + "release_date": "2019-09-13", + "server": { + "kb_version": { + "daily": "23.03.19", + "monthly": "23.03" + }, + "version": "5.2.5" + }, + "source_hash": "5e9fbaa3bde70a3a5a077fe6d65e36de", + "status": "pending", + "url": "https://github.com/vdurmont/semver4j", + "url_hash": "b92cd7e4d588747588d1b5fe1f8c664c", + "vendor": "vdurmont", + "version": "3.1.0" + } + ], + "src/test/java/com/vdurmont/semver4j/SemverTest.java": [ + { + "component": "semver4j", + "file": "src/test/java/com/vdurmont/semver4j/SemverTest.java", + "file_hash": "ef32d9061afb0be4d07285813321e306", + "file_url": "https://osskb.org/api/file_contents/ef32d9061afb0be4d07285813321e306", + "id": "file", + "latest": "3.1.0", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/MIT.txt", + "copyleft": "no", + "name": "MIT", + "osadl_updated": "2023-03-26T02:11:00+00:00", + "patent_hints": "no", + "source": "component_declared", + "url": "https://spdx.org/licenses/MIT.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/vdurmont/semver4j" + ], + "release_date": "2019-09-13", + "server": { + "kb_version": { + "daily": "23.03.19", + "monthly": "23.03" + }, + "version": "5.2.5" + }, + "source_hash": "ef32d9061afb0be4d07285813321e306", + "status": "pending", + "url": "https://github.com/vdurmont/semver4j", + "url_hash": "b92cd7e4d588747588d1b5fe1f8c664c", + "vendor": "vdurmont", + "version": "3.1.0" + } + ], + "src/test/java/com/vdurmont/semver4j/TokenizerTest.java": [ + { + "component": "semver4j", + "file": "src/test/java/com/vdurmont/semver4j/TokenizerTest.java", + "file_hash": "4498810681d4cbdc111ddbf4015b351e", + "file_url": "https://osskb.org/api/file_contents/4498810681d4cbdc111ddbf4015b351e", + "id": "file", + "latest": "3.1.0", + "licenses": [ + { + "checklist_url": "https://www.osadl.org/fileadmin/checklists/unreflicenses/MIT.txt", + "copyleft": "no", + "name": "MIT", + "osadl_updated": "2023-03-26T02:11:00+00:00", + "patent_hints": "no", + "source": "component_declared", + "url": "https://spdx.org/licenses/MIT.html" + } + ], + "lines": "all", + "matched": "100%", + "oss_lines": "all", + "purl": [ + "pkg:github/vdurmont/semver4j" + ], + "release_date": "2019-09-13", + "server": { + "kb_version": { + "daily": "23.03.19", + "monthly": "23.03" + }, + "version": "5.2.5" + }, + "source_hash": "4498810681d4cbdc111ddbf4015b351e", + "status": "pending", + "url": "https://github.com/vdurmont/semver4j", + "url_hash": "b92cd7e4d588747588d1b5fe1f8c664c", + "vendor": "vdurmont", + "version": "3.1.0" + } + ] +} diff --git a/scanner/src/test/kotlin/scanners/scanoss/ScanOssResultParserTest.kt b/scanner/src/test/kotlin/scanners/scanoss/ScanOssResultParserTest.kt index c69b472365b37..8dbb0ed38d80f 100644 --- a/scanner/src/test/kotlin/scanners/scanoss/ScanOssResultParserTest.kt +++ b/scanner/src/test/kotlin/scanners/scanoss/ScanOssResultParserTest.kt @@ -23,6 +23,8 @@ import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.collections.containExactlyInAnyOrder import io.kotest.matchers.collections.haveSize import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.should import java.io.File @@ -34,8 +36,14 @@ import org.ossreviewtoolkit.clients.scanoss.FullScanResponse import org.ossreviewtoolkit.clients.scanoss.ScanOssService import org.ossreviewtoolkit.model.CopyrightFinding import org.ossreviewtoolkit.model.LicenseFinding +import org.ossreviewtoolkit.model.RepositoryProvenance import org.ossreviewtoolkit.model.TextLocation +import org.ossreviewtoolkit.model.VcsInfo +import org.ossreviewtoolkit.model.VcsType +import org.ossreviewtoolkit.model.utils.Snippet +import org.ossreviewtoolkit.model.utils.SnippetFinding import org.ossreviewtoolkit.utils.spdx.SpdxConstants +import org.ossreviewtoolkit.utils.spdx.SpdxExpression class ScanOssResultParserTest : WordSpec({ "generateSummary()" should { @@ -76,5 +84,54 @@ class ScanOssResultParserTest : WordSpec({ ) ) } + + "properly summarize Semver4j 3.1.0 with snippet findings" { + val result = File("src/test/assets/scanoss-semver4j-3.1.0-with-snippet.json").inputStream().use { + ScanOssService.JSON.decodeFromStream(it) + } + + val time = Instant.now() + val summary = generateSummary(time, time, SpdxConstants.NONE, result, emptyMap()) + + summary.licenses.map { it.toString() } should containExactlyInAnyOrder( + "Apache-2.0", + "BSD-2-Clause", + "EPL-2.0", + "LicenseRef-scanoss-SSPL", + "MIT" + ) + + summary.licenseFindings should haveSize(11) + summary.licenseFindings shouldContain LicenseFinding( + license = "Apache-2.0", + location = TextLocation( + path = "com/vdurmont/semver4j/Range.java", + startLine = TextLocation.UNKNOWN_LINE, + endLine = TextLocation.UNKNOWN_LINE + ), + score = 100.0f + ) + + summary.snippetFindings shouldHaveSize (1) + summary.snippetFindings.shouldContainExactly( + SnippetFinding( + TextLocation("src/main/java/com/vdurmont/semver4j/Requirement.java", 1, 710), + Snippet( + 98.0f, + TextLocation( + "https://osskb.org/api/file_contents/6ff2427335b985212c9b79dfa795799f", + 1, + 710 + ), + RepositoryProvenance( + VcsInfo(VcsType.UNKNOWN, "https://github.com/vdurmont/semver4j", ""), + "." + ), + "pkg:github/vdurmont/semver4j", + SpdxExpression.parse("CC-BY-SA-2.0") + ) + ) + ) + } } }) diff --git a/scanner/src/test/kotlin/scanners/scanoss/ScanOssScannerDirectoryTest.kt b/scanner/src/test/kotlin/scanners/scanoss/ScanOssScannerDirectoryTest.kt index 025047b26959f..65662d15bd0b4 100644 --- a/scanner/src/test/kotlin/scanners/scanoss/ScanOssScannerDirectoryTest.kt +++ b/scanner/src/test/kotlin/scanners/scanoss/ScanOssScannerDirectoryTest.kt @@ -24,6 +24,7 @@ import com.github.tomakehurst.wiremock.core.WireMockConfiguration import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.containExactlyInAnyOrder +import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.should import io.mockk.every @@ -35,10 +36,16 @@ import java.util.UUID import org.ossreviewtoolkit.model.LicenseFinding import org.ossreviewtoolkit.model.PackageType +import org.ossreviewtoolkit.model.RepositoryProvenance import org.ossreviewtoolkit.model.TextLocation +import org.ossreviewtoolkit.model.VcsInfo +import org.ossreviewtoolkit.model.VcsType import org.ossreviewtoolkit.model.config.DownloaderConfiguration import org.ossreviewtoolkit.model.config.ScannerConfiguration +import org.ossreviewtoolkit.model.utils.Snippet +import org.ossreviewtoolkit.model.utils.SnippetFinding import org.ossreviewtoolkit.scanner.ScanContext +import org.ossreviewtoolkit.utils.spdx.SpdxExpression private val TEST_DIRECTORY_TO_SCAN = File("src/test/assets/scanoss/filesToScan") @@ -91,14 +98,6 @@ class ScanOssScannerDirectoryTest : StringSpec({ with(summary) { licenseFindings should containExactlyInAnyOrder( - LicenseFinding( - license = "Apache-2.0", - location = TextLocation( - path = "utils/src/main/kotlin/ArchiveUtils.kt", - line = TextLocation.UNKNOWN_LINE - ), - score = 99.0f - ), LicenseFinding( license = "Apache-2.0", location = TextLocation( @@ -108,6 +107,25 @@ class ScanOssScannerDirectoryTest : StringSpec({ score = 100.0f ) ) + + snippetFindings.shouldContainExactly( + SnippetFinding( + TextLocation("utils/src/main/kotlin/ArchiveUtils.kt", 1, 240), + Snippet( + 99.0f, + TextLocation( + "https://osskb.org/api/file_contents/871fb0c5188c2f620d9b997e225b0095", + 128, + 367 + ), + RepositoryProvenance( + VcsInfo(VcsType.UNKNOWN, "https://github.com/scanoss/ort", ""), "." + ), + "pkg:github/scanoss/ort", + SpdxExpression.parse("Apache-2.0") + ) + ) + ) } } })