Skip to content

Commit 3c61670

Browse files
committed
feat(fossid-webapp): Map FossID snippets to the ScanSummary
When FossId identifies a file matching snippets, it is a pending file. An operator needs to log to FossID UI and use the license of a snippet or manually enter difference license information. Then the file is marked as "identified". Currently, the FossID scanner in ORT returns the list of all pending files in `ScanSummary` issues, with a severity of `HINT`. This commit maps the snippets of pending files using the newly-created snippet data model. The pending files are still listed as issues: this will be removed in a future commit as it is a breaking change. Signed-off-by: Nicolas Nobelis <[email protected]>
1 parent f1eabc5 commit 3c61670

File tree

3 files changed

+147
-4
lines changed

3 files changed

+147
-4
lines changed

scanner/src/main/kotlin/scanners/fossid/FossId.kt

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import kotlin.time.Duration.Companion.minutes
2727
import kotlin.time.Duration.Companion.seconds
2828
import kotlin.time.measureTimedValue
2929

30+
import kotlinx.coroutines.Dispatchers
31+
import kotlinx.coroutines.async
32+
import kotlinx.coroutines.awaitAll
3033
import kotlinx.coroutines.delay
3134
import kotlinx.coroutines.runBlocking
3235
import kotlinx.coroutines.withTimeoutOrNull
@@ -48,6 +51,7 @@ import org.ossreviewtoolkit.clients.fossid.listIgnoredFiles
4851
import org.ossreviewtoolkit.clients.fossid.listMarkedAsIdentifiedFiles
4952
import org.ossreviewtoolkit.clients.fossid.listPendingFiles
5053
import org.ossreviewtoolkit.clients.fossid.listScansForProject
54+
import org.ossreviewtoolkit.clients.fossid.listSnippets
5155
import org.ossreviewtoolkit.clients.fossid.model.Project
5256
import org.ossreviewtoolkit.clients.fossid.model.Scan
5357
import org.ossreviewtoolkit.clients.fossid.model.rules.RuleScope
@@ -64,12 +68,14 @@ import org.ossreviewtoolkit.model.ScanResult
6468
import org.ossreviewtoolkit.model.ScanSummary
6569
import org.ossreviewtoolkit.model.ScannerDetails
6670
import org.ossreviewtoolkit.model.Severity
71+
import org.ossreviewtoolkit.model.TextLocation
6772
import org.ossreviewtoolkit.model.UnknownProvenance
6873
import org.ossreviewtoolkit.model.VcsType
6974
import org.ossreviewtoolkit.model.config.DownloaderConfiguration
7075
import org.ossreviewtoolkit.model.config.Options
7176
import org.ossreviewtoolkit.model.config.ScannerConfiguration
7277
import org.ossreviewtoolkit.model.createAndLogIssue
78+
import org.ossreviewtoolkit.model.utils.SnippetFinding
7379
import org.ossreviewtoolkit.scanner.AbstractScannerWrapperFactory
7480
import org.ossreviewtoolkit.scanner.PackageScannerWrapper
7581
import org.ossreviewtoolkit.scanner.ProvenanceScannerWrapper
@@ -78,6 +84,9 @@ import org.ossreviewtoolkit.scanner.ScannerCriteria
7884
import org.ossreviewtoolkit.utils.common.enumSetOf
7985
import org.ossreviewtoolkit.utils.common.replaceCredentialsInUri
8086
import org.ossreviewtoolkit.utils.ort.showStackTrace
87+
import org.ossreviewtoolkit.utils.spdx.SpdxConstants
88+
import org.ossreviewtoolkit.utils.spdx.SpdxExpression
89+
import org.ossreviewtoolkit.utils.spdx.toSpdx
8190

8291
/**
8392
* A wrapper for [FossID](https://fossid.com/).
@@ -746,7 +755,23 @@ class FossId internal constructor(
746755
"${pendingFiles.size} pending files have been returned for scan '$scanCode'."
747756
}
748757

749-
return RawResults(identifiedFiles, markedAsIdentifiedFiles, listIgnoredFiles, pendingFiles)
758+
val snippets = runBlocking(Dispatchers.IO) {
759+
pendingFiles.map {
760+
logger.info { "Listing snippet for $it..." }
761+
async {
762+
val snippetResponse = service.listSnippets(config.user, config.apiKey, scanCode, it)
763+
.checkResponse("list snippets")
764+
val snippets = requireNotNull(snippetResponse.data) {
765+
"Snippet could not be listed. Response was ${snippetResponse.message}."
766+
}
767+
logger.info { "${snippets.size} snippets." }
768+
769+
it to snippets.toSet()
770+
}
771+
}.awaitAll().toMap()
772+
}
773+
774+
return RawResults(identifiedFiles, markedAsIdentifiedFiles, listIgnoredFiles, pendingFiles, snippets)
750775
}
751776

752777
/**
@@ -760,10 +785,43 @@ class FossId internal constructor(
760785
scanId: String
761786
): ScanResult {
762787
// TODO: Maybe get issues from FossID (see has_failed_scan_files, get_failed_files and maybe get_scan_log).
788+
789+
// TODO: Deprecation: Remove the pending files in issues. This is a breaking change.
763790
val issues = rawResults.listPendingFiles.mapTo(mutableListOf()) {
764791
Issue(source = name, message = "Pending identification for '$it'.", severity = Severity.HINT)
765792
}
766793

794+
val snippets = rawResults.listSnippets.mapValues { entry ->
795+
entry.value.map {
796+
val license = it.artifactLicense?.let { license ->
797+
val licenseExpression = runCatching { SpdxExpression.parse(license) }.getOrNull()
798+
799+
val validatedLicense = when {
800+
licenseExpression == null -> SpdxConstants.NOASSERTION.toSpdx()
801+
licenseExpression.isValid() -> licenseExpression
802+
else -> "${SpdxConstants.LICENSE_REF_PREFIX}scanoss-$license".toSpdx()
803+
}
804+
805+
setOf(validatedLicense)
806+
} ?: emptySet()
807+
// TODO: FossId doesn't return the line numbers of the match, only the character range. One must use
808+
// another call "getMatchedLine" to retrieve the matched line numbers. Unfortunately, this is a call
809+
// per snippet which is too expensive. When it is available for a batch of snippets, it can be used
810+
// here.
811+
val sourceLocation = TextLocation(it.file, -1)
812+
val snippetLocation = TextLocation(it.url.orEmpty(), -1)
813+
SnippetFinding(
814+
it.author,
815+
it.artifact,
816+
it.version,
817+
license,
818+
it.score.toFloat(),
819+
sourceLocation,
820+
snippetLocation
821+
)
822+
}.toSet()
823+
}
824+
767825
val ignoredFiles = rawResults.listIgnoredFiles.associateBy { it.path }
768826

769827
val (licenseFindings, copyrightFindings) = rawResults.markedAsIdentifiedFiles.ifEmpty {
@@ -776,6 +834,7 @@ class FossId internal constructor(
776834
packageVerificationCode = "",
777835
licenseFindings = licenseFindings.toSortedSet(),
778836
copyrightFindings = copyrightFindings.toSortedSet(),
837+
snippetFindings = snippets,
779838
issues = issues
780839
)
781840

scanner/src/main/kotlin/scanners/fossid/FossIdScanResults.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package org.ossreviewtoolkit.scanner.scanners.fossid
2222
import org.ossreviewtoolkit.clients.fossid.model.identification.identifiedFiles.IdentifiedFile
2323
import org.ossreviewtoolkit.clients.fossid.model.identification.ignored.IgnoredFile
2424
import org.ossreviewtoolkit.clients.fossid.model.identification.markedAsIdentified.MarkedAsIdentifiedFile
25+
import org.ossreviewtoolkit.clients.fossid.model.result.Snippet
2526
import org.ossreviewtoolkit.clients.fossid.model.summary.Summarizable
2627
import org.ossreviewtoolkit.model.CopyrightFinding
2728
import org.ossreviewtoolkit.model.Issue
@@ -37,7 +38,8 @@ internal data class RawResults(
3738
val identifiedFiles: List<IdentifiedFile>,
3839
val markedAsIdentifiedFiles: List<MarkedAsIdentifiedFile>,
3940
val listIgnoredFiles: List<IgnoredFile>,
40-
val listPendingFiles: List<String>
41+
val listPendingFiles: List<String>,
42+
val listSnippets: Map<String, Set<Snippet>>
4143
)
4244

4345
/**

scanner/src/test/kotlin/scanners/fossid/FossIdTest.kt

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,16 @@ import org.ossreviewtoolkit.clients.fossid.listIgnoredFiles
6262
import org.ossreviewtoolkit.clients.fossid.listMarkedAsIdentifiedFiles
6363
import org.ossreviewtoolkit.clients.fossid.listPendingFiles
6464
import org.ossreviewtoolkit.clients.fossid.listScansForProject
65+
import org.ossreviewtoolkit.clients.fossid.listSnippets
6566
import org.ossreviewtoolkit.clients.fossid.model.Scan
6667
import org.ossreviewtoolkit.clients.fossid.model.identification.common.LicenseMatchType
6768
import org.ossreviewtoolkit.clients.fossid.model.identification.identifiedFiles.IdentifiedFile
6869
import org.ossreviewtoolkit.clients.fossid.model.identification.ignored.IgnoredFile
6970
import org.ossreviewtoolkit.clients.fossid.model.identification.markedAsIdentified.License
7071
import org.ossreviewtoolkit.clients.fossid.model.identification.markedAsIdentified.LicenseFile
7172
import org.ossreviewtoolkit.clients.fossid.model.identification.markedAsIdentified.MarkedAsIdentifiedFile
73+
import org.ossreviewtoolkit.clients.fossid.model.result.MatchType
74+
import org.ossreviewtoolkit.clients.fossid.model.result.Snippet
7275
import org.ossreviewtoolkit.clients.fossid.model.rules.IgnoreRule
7376
import org.ossreviewtoolkit.clients.fossid.model.rules.RuleScope
7477
import org.ossreviewtoolkit.clients.fossid.model.rules.RuleType
@@ -90,11 +93,13 @@ import org.ossreviewtoolkit.model.TextLocation
9093
import org.ossreviewtoolkit.model.VcsInfo
9194
import org.ossreviewtoolkit.model.VcsType
9295
import org.ossreviewtoolkit.model.config.ScannerConfiguration
96+
import org.ossreviewtoolkit.model.utils.SnippetFinding
9397
import org.ossreviewtoolkit.scanner.ScanContext
9498
import org.ossreviewtoolkit.scanner.scanners.fossid.FossId.Companion.SCAN_CODE_KEY
9599
import org.ossreviewtoolkit.scanner.scanners.fossid.FossId.Companion.SCAN_ID_KEY
96100
import org.ossreviewtoolkit.scanner.scanners.fossid.FossId.Companion.SERVER_URL_KEY
97101
import org.ossreviewtoolkit.scanner.scanners.fossid.FossId.Companion.convertGitUrlToProjectName
102+
import org.ossreviewtoolkit.utils.spdx.SpdxExpression
98103

99104
@Suppress("LargeClass")
100105
class FossIdTest : WordSpec({
@@ -314,6 +319,7 @@ class FossIdTest : WordSpec({
314319
summary.licenseFindings shouldContainExactlyInAnyOrder expectedLicenseFindings
315320
}
316321

322+
// TODO: Deprecation: Remove the pending files in issues. This is a breaking change.
317323
"report pending files as issues" {
318324
val projectCode = projectCode(PROJECT)
319325
val scanCode = scanCode(PROJECT, null)
@@ -328,7 +334,7 @@ class FossIdTest : WordSpec({
328334
.expectCheckScanStatus(scanCode, ScanStatus.FINISHED)
329335
.expectCreateScan(projectCode, scanCode, vcsInfo, "")
330336
.expectDownload(scanCode)
331-
.mockFiles(scanCode, pendingRange = 4..5)
337+
.mockFiles(scanCode, pendingRange = 4..5, snippetRange = 1..5)
332338

333339
val fossId = createFossId(config)
334340

@@ -341,6 +347,35 @@ class FossIdTest : WordSpec({
341347
summary.issues.map { it.copy(timestamp = Instant.EPOCH) } shouldBe expectedIssues
342348
}
343349

350+
"report pending files as snippets" {
351+
val projectCode = projectCode(PROJECT)
352+
val scanCode = scanCode(PROJECT, null)
353+
val config = createConfig(deltaScans = false)
354+
val vcsInfo = createVcsInfo()
355+
val scan = createScan(vcsInfo.url, "${vcsInfo.revision}_other", scanCode)
356+
val pkgId = createIdentifier(index = 42)
357+
358+
FossIdRestService.create(config.serverUrl)
359+
.expectProjectRequest(projectCode)
360+
.expectListScans(projectCode, listOf(scan))
361+
.expectCheckScanStatus(scanCode, ScanStatus.FINISHED)
362+
.expectCreateScan(projectCode, scanCode, vcsInfo, "")
363+
.expectDownload(scanCode)
364+
.mockFiles(scanCode, pendingRange = 1..5, snippetRange = 1..5)
365+
366+
val fossId = createFossId(config)
367+
368+
val summary = fossId.scan(createPackage(pkgId, vcsInfo)).summary
369+
370+
val expectedPendingFile = (1..5).map(::createPendingFile).toSet()
371+
val expectedSnippetFindings = (1..5).map(::createSnippetFinding).toSet()
372+
373+
summary.snippetFindings.keys shouldBe expectedPendingFile
374+
summary.snippetFindings.values.forEach {
375+
it shouldBe expectedSnippetFindings
376+
}
377+
}
378+
344379
"create a new project if none exists yet" {
345380
val projectCode = projectCode(PROJECT)
346381
val scanCode = scanCode(PROJECT, null)
@@ -1238,6 +1273,49 @@ private fun createIgnoredFile(index: Int): IgnoredFile =
12381273
*/
12391274
private fun createPendingFile(index: Int): String = "/pending/file/$index"
12401275

1276+
/**
1277+
* Generate a FossID snippet based on the given [index].
1278+
*/
1279+
private fun createSnippet(index: Int): Snippet = Snippet(
1280+
index,
1281+
"created$index",
1282+
index,
1283+
index,
1284+
index,
1285+
MatchType.PARTIAL,
1286+
"reason$index",
1287+
"author$index",
1288+
"artifact$index",
1289+
"version$index",
1290+
"MIT",
1291+
"releaseDate$index",
1292+
"mirror$index",
1293+
"file$index",
1294+
"fileLicense$index",
1295+
"url$index",
1296+
"hits$index",
1297+
index,
1298+
"updated$index",
1299+
"cpe$index",
1300+
"$index",
1301+
"matchField$index",
1302+
"classification$index",
1303+
"highlighting$index"
1304+
)
1305+
1306+
/**
1307+
* Generate a ORT snippet finding based on the given [index].
1308+
*/
1309+
private fun createSnippetFinding(index: Int): SnippetFinding = SnippetFinding(
1310+
"author$index",
1311+
"artifact$index",
1312+
"version$index",
1313+
setOf(SpdxExpression.Companion.parse("MIT")),
1314+
index.toFloat(),
1315+
TextLocation("file$index", -1),
1316+
TextLocation("url$index", -1)
1317+
)
1318+
12411319
/**
12421320
* Prepare this service mock to answer a request for a project with the given [projectCode]. Return a response with
12431321
* the given [status] and [error].
@@ -1348,12 +1426,14 @@ private fun FossIdServiceWithVersion.mockFiles(
13481426
identifiedRange: IntRange = IntRange.EMPTY,
13491427
markedRange: IntRange = IntRange.EMPTY,
13501428
ignoredRange: IntRange = IntRange.EMPTY,
1351-
pendingRange: IntRange = IntRange.EMPTY
1429+
pendingRange: IntRange = IntRange.EMPTY,
1430+
snippetRange: IntRange = IntRange.EMPTY
13521431
): FossIdServiceWithVersion {
13531432
val identifiedFiles = identifiedRange.map(::createIdentifiedFile)
13541433
val markedFiles = markedRange.map(::createMarkedIdentifiedFile)
13551434
val ignoredFiles = ignoredRange.map(::createIgnoredFile)
13561435
val pendingFiles = pendingRange.map(::createPendingFile)
1436+
val snippets = snippetRange.map(::createSnippet)
13571437

13581438
coEvery { listIdentifiedFiles(USER, API_KEY, scanCode) } returns
13591439
PolymorphicResponseBody(
@@ -1367,6 +1447,8 @@ private fun FossIdServiceWithVersion.mockFiles(
13671447
PolymorphicResponseBody(status = 1, data = PolymorphicList(ignoredFiles))
13681448
coEvery { listPendingFiles(USER, API_KEY, scanCode) } returns
13691449
PolymorphicResponseBody(status = 1, data = PolymorphicList(pendingFiles))
1450+
coEvery { listSnippets(USER, API_KEY, scanCode, any()) } returns
1451+
PolymorphicResponseBody(status = 1, data = PolymorphicList(snippets))
13701452

13711453
return this
13721454
}

0 commit comments

Comments
 (0)