Skip to content

Commit fc090ad

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 bc085ad commit fc090ad

File tree

3 files changed

+157
-4
lines changed

3 files changed

+157
-4
lines changed

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

Lines changed: 65 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
@@ -56,20 +60,25 @@ import org.ossreviewtoolkit.clients.fossid.model.status.DownloadStatus
5660
import org.ossreviewtoolkit.clients.fossid.model.status.ScanStatus
5761
import org.ossreviewtoolkit.clients.fossid.runScan
5862
import org.ossreviewtoolkit.downloader.VersionControlSystem
63+
import org.ossreviewtoolkit.model.Hash
5964
import org.ossreviewtoolkit.model.Issue
6065
import org.ossreviewtoolkit.model.Package
6166
import org.ossreviewtoolkit.model.Provenance
67+
import org.ossreviewtoolkit.model.RemoteArtifact
6268
import org.ossreviewtoolkit.model.RepositoryProvenance
6369
import org.ossreviewtoolkit.model.ScanResult
6470
import org.ossreviewtoolkit.model.ScanSummary
6571
import org.ossreviewtoolkit.model.ScannerDetails
6672
import org.ossreviewtoolkit.model.Severity
73+
import org.ossreviewtoolkit.model.TextLocation
6774
import org.ossreviewtoolkit.model.UnknownProvenance
6875
import org.ossreviewtoolkit.model.VcsType
6976
import org.ossreviewtoolkit.model.config.DownloaderConfiguration
7077
import org.ossreviewtoolkit.model.config.Options
7178
import org.ossreviewtoolkit.model.config.ScannerConfiguration
7279
import org.ossreviewtoolkit.model.createAndLogIssue
80+
import org.ossreviewtoolkit.model.utils.DetectedSnippet
81+
import org.ossreviewtoolkit.model.utils.SnippetFinding
7382
import org.ossreviewtoolkit.scanner.AbstractScannerWrapperFactory
7483
import org.ossreviewtoolkit.scanner.PackageScannerWrapper
7584
import org.ossreviewtoolkit.scanner.ProvenanceScannerWrapper
@@ -78,6 +87,9 @@ import org.ossreviewtoolkit.scanner.ScannerCriteria
7887
import org.ossreviewtoolkit.utils.common.enumSetOf
7988
import org.ossreviewtoolkit.utils.common.replaceCredentialsInUri
8089
import org.ossreviewtoolkit.utils.ort.showStackTrace
90+
import org.ossreviewtoolkit.utils.spdx.SpdxConstants
91+
import org.ossreviewtoolkit.utils.spdx.SpdxExpression
92+
import org.ossreviewtoolkit.utils.spdx.toSpdx
8193

8294
/**
8395
* A wrapper for [FossID](https://fossid.com/).
@@ -746,7 +758,23 @@ class FossId internal constructor(
746758
"${pendingFiles.size} pending files have been returned for scan '$scanCode'."
747759
}
748760

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

752780
/**
@@ -760,10 +788,45 @@ class FossId internal constructor(
760788
scanId: String
761789
): ScanResult {
762790
// TODO: Maybe get issues from FossID (see has_failed_scan_files, get_failed_files and maybe get_scan_log).
791+
792+
// TODO: Deprecation: Remove the pending files in issues. This is a breaking change.
763793
val issues = rawResults.listPendingFiles.mapTo(mutableListOf()) {
764794
Issue(source = name, message = "Pending identification for '$it'.", severity = Severity.HINT)
765795
}
766796

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

769832
val (licenseFindings, copyrightFindings) = rawResults.markedAsIdentifiedFiles.ifEmpty {
@@ -776,6 +839,7 @@ class FossId internal constructor(
776839
packageVerificationCode = "",
777840
licenseFindings = licenseFindings.toSortedSet(),
778841
copyrightFindings = copyrightFindings.toSortedSet(),
842+
snippetFindings = snippets,
779843
issues = issues
780844
)
781845

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: 89 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
@@ -79,22 +82,27 @@ import org.ossreviewtoolkit.clients.fossid.runScan
7982
import org.ossreviewtoolkit.downloader.VersionControlSystem
8083
import org.ossreviewtoolkit.downloader.vcs.Git
8184
import org.ossreviewtoolkit.model.CopyrightFinding
85+
import org.ossreviewtoolkit.model.Hash
8286
import org.ossreviewtoolkit.model.Identifier
8387
import org.ossreviewtoolkit.model.Issue
8488
import org.ossreviewtoolkit.model.LicenseFinding
8589
import org.ossreviewtoolkit.model.Package
8690
import org.ossreviewtoolkit.model.PackageType
91+
import org.ossreviewtoolkit.model.RemoteArtifact
8792
import org.ossreviewtoolkit.model.ScanResult
8893
import org.ossreviewtoolkit.model.Severity
8994
import org.ossreviewtoolkit.model.TextLocation
9095
import org.ossreviewtoolkit.model.VcsInfo
9196
import org.ossreviewtoolkit.model.VcsType
9297
import org.ossreviewtoolkit.model.config.ScannerConfiguration
98+
import org.ossreviewtoolkit.model.utils.DetectedSnippet
99+
import org.ossreviewtoolkit.model.utils.SnippetFinding
93100
import org.ossreviewtoolkit.scanner.ScanContext
94101
import org.ossreviewtoolkit.scanner.scanners.fossid.FossId.Companion.SCAN_CODE_KEY
95102
import org.ossreviewtoolkit.scanner.scanners.fossid.FossId.Companion.SCAN_ID_KEY
96103
import org.ossreviewtoolkit.scanner.scanners.fossid.FossId.Companion.SERVER_URL_KEY
97104
import org.ossreviewtoolkit.scanner.scanners.fossid.FossId.Companion.convertGitUrlToProjectName
105+
import org.ossreviewtoolkit.utils.spdx.SpdxExpression
98106

99107
@Suppress("LargeClass")
100108
class FossIdTest : WordSpec({
@@ -314,6 +322,7 @@ class FossIdTest : WordSpec({
314322
summary.licenseFindings shouldContainExactlyInAnyOrder expectedLicenseFindings
315323
}
316324

325+
// TODO: Deprecation: Remove the pending files in issues. This is a breaking change.
317326
"report pending files as issues" {
318327
val projectCode = projectCode(PROJECT)
319328
val scanCode = scanCode(PROJECT, null)
@@ -328,7 +337,7 @@ class FossIdTest : WordSpec({
328337
.expectCheckScanStatus(scanCode, ScanStatus.FINISHED)
329338
.expectCreateScan(projectCode, scanCode, vcsInfo, "")
330339
.expectDownload(scanCode)
331-
.mockFiles(scanCode, pendingRange = 4..5)
340+
.mockFiles(scanCode, pendingRange = 4..5, snippetRange = 1..5)
332341

333342
val fossId = createFossId(config)
334343

@@ -341,6 +350,35 @@ class FossIdTest : WordSpec({
341350
summary.issues.map { it.copy(timestamp = Instant.EPOCH) } shouldBe expectedIssues
342351
}
343352

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

1279+
/**
1280+
* Generate a FossID snippet based on the given [index].
1281+
*/
1282+
private fun createSnippet(index: Int): Snippet = Snippet(
1283+
index,
1284+
"created$index",
1285+
index,
1286+
index,
1287+
index,
1288+
MatchType.PARTIAL,
1289+
"reason$index",
1290+
"author$index",
1291+
"artifact$index",
1292+
"version$index",
1293+
"MIT",
1294+
"releaseDate$index",
1295+
"mirror$index",
1296+
"file$index",
1297+
"fileLicense$index",
1298+
"url$index",
1299+
"hits$index",
1300+
index,
1301+
"updated$index",
1302+
"cpe$index",
1303+
"$index",
1304+
"matchField$index",
1305+
"classification$index",
1306+
"highlighting$index"
1307+
)
1308+
1309+
/**
1310+
* Generate a ORT snippet finding based on the given [index].
1311+
*/
1312+
private fun createSnippetFinding(index: Int): SnippetFinding = SnippetFinding(
1313+
index.toFloat(),
1314+
TextLocation("file$index", TextLocation.UNKNOWN_LINE),
1315+
DetectedSnippet(
1316+
TextLocation("url$index", TextLocation.UNKNOWN_LINE),
1317+
null,
1318+
RemoteArtifact("url$index", Hash.NONE),
1319+
setOf("pkg:fossid/author$index/artifact$index@version$index"),
1320+
SpdxExpression.Companion.parse("MIT")
1321+
)
1322+
)
1323+
12411324
/**
12421325
* Prepare this service mock to answer a request for a project with the given [projectCode]. Return a response with
12431326
* the given [status] and [error].
@@ -1348,12 +1431,14 @@ private fun FossIdServiceWithVersion.mockFiles(
13481431
identifiedRange: IntRange = IntRange.EMPTY,
13491432
markedRange: IntRange = IntRange.EMPTY,
13501433
ignoredRange: IntRange = IntRange.EMPTY,
1351-
pendingRange: IntRange = IntRange.EMPTY
1434+
pendingRange: IntRange = IntRange.EMPTY,
1435+
snippetRange: IntRange = IntRange.EMPTY
13521436
): FossIdServiceWithVersion {
13531437
val identifiedFiles = identifiedRange.map(::createIdentifiedFile)
13541438
val markedFiles = markedRange.map(::createMarkedIdentifiedFile)
13551439
val ignoredFiles = ignoredRange.map(::createIgnoredFile)
13561440
val pendingFiles = pendingRange.map(::createPendingFile)
1441+
val snippets = snippetRange.map(::createSnippet)
13571442

13581443
coEvery { listIdentifiedFiles(USER, API_KEY, scanCode) } returns
13591444
PolymorphicResponseBody(
@@ -1367,6 +1452,8 @@ private fun FossIdServiceWithVersion.mockFiles(
13671452
PolymorphicResponseBody(status = 1, data = PolymorphicList(ignoredFiles))
13681453
coEvery { listPendingFiles(USER, API_KEY, scanCode) } returns
13691454
PolymorphicResponseBody(status = 1, data = PolymorphicList(pendingFiles))
1455+
coEvery { listSnippets(USER, API_KEY, scanCode, any()) } returns
1456+
PolymorphicResponseBody(status = 1, data = PolymorphicList(snippets))
13701457

13711458
return this
13721459
}

0 commit comments

Comments
 (0)