-
Notifications
You must be signed in to change notification settings - Fork 381
Expand file tree
/
Copy pathSpdxDocumentFile.kt
More file actions
400 lines (351 loc) · 17.2 KB
/
SpdxDocumentFile.kt
File metadata and controls
400 lines (351 loc) · 17.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
/*
* Copyright (C) 2020 The ORT Project Copyright Holders <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>
*
* 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.packagemanagers.spdxdocumentfile
import java.io.File
import org.apache.logging.log4j.kotlin.logger
import org.ossreviewtoolkit.analyzer.PackageManager
import org.ossreviewtoolkit.analyzer.PackageManagerDependency
import org.ossreviewtoolkit.analyzer.PackageManagerFactory
import org.ossreviewtoolkit.analyzer.PackageManagerResult
import org.ossreviewtoolkit.analyzer.determineEnabledPackageManagers
import org.ossreviewtoolkit.analyzer.toPackageReference
import org.ossreviewtoolkit.downloader.VersionControlSystem
import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.Issue
import org.ossreviewtoolkit.model.Package
import org.ossreviewtoolkit.model.PackageLinkage
import org.ossreviewtoolkit.model.PackageReference
import org.ossreviewtoolkit.model.Project
import org.ossreviewtoolkit.model.ProjectAnalyzerResult
import org.ossreviewtoolkit.model.Scope
import org.ossreviewtoolkit.model.VcsInfo
import org.ossreviewtoolkit.model.config.AnalyzerConfiguration
import org.ossreviewtoolkit.model.config.Excludes
import org.ossreviewtoolkit.model.config.Includes
import org.ossreviewtoolkit.model.createAndLogIssue
import org.ossreviewtoolkit.model.utils.toIdentifier
import org.ossreviewtoolkit.model.utils.toPackageUrl
import org.ossreviewtoolkit.plugins.api.OrtPlugin
import org.ossreviewtoolkit.plugins.api.OrtPluginOption
import org.ossreviewtoolkit.plugins.api.PluginConfig
import org.ossreviewtoolkit.plugins.api.PluginDescriptor
import org.ossreviewtoolkit.plugins.packagemanagers.spdxdocumentfile.utils.SpdxDocumentCache
import org.ossreviewtoolkit.plugins.packagemanagers.spdxdocumentfile.utils.SpdxResolvedDocument
import org.ossreviewtoolkit.plugins.packagemanagers.spdxdocumentfile.utils.extractScopeFromExternalReferences
import org.ossreviewtoolkit.plugins.packagemanagers.spdxdocumentfile.utils.isExternalDocumentReferenceId
import org.ossreviewtoolkit.plugins.packagemanagers.spdxdocumentfile.utils.locateCpe
import org.ossreviewtoolkit.plugins.packagemanagers.spdxdocumentfile.utils.locateExternalReference
import org.ossreviewtoolkit.plugins.packagemanagers.spdxdocumentfile.utils.mapNotPresentToEmpty
import org.ossreviewtoolkit.plugins.packagemanagers.spdxdocumentfile.utils.projectPackage
import org.ossreviewtoolkit.plugins.packagemanagers.spdxdocumentfile.utils.toIdentifier
import org.ossreviewtoolkit.plugins.packagemanagers.spdxdocumentfile.utils.toPackage
import org.ossreviewtoolkit.plugins.packagemanagers.spdxdocumentfile.utils.wrapPresentInSet
import org.ossreviewtoolkit.utils.spdxdocument.model.SpdxExternalReference
import org.ossreviewtoolkit.utils.spdxdocument.model.SpdxPackage
import org.ossreviewtoolkit.utils.spdxdocument.model.SpdxRelationship
private const val PROJECT_TYPE = "SpdxDocumentFile"
internal const val PACKAGE_TYPE_SPDX = "SpdxDocumentFile"
private const val DEFAULT_SCOPE_NAME = "default"
private val SPDX_LINKAGE_RELATIONSHIPS = mapOf(
SpdxRelationship.Type.DYNAMIC_LINK to PackageLinkage.DYNAMIC,
SpdxRelationship.Type.STATIC_LINK to PackageLinkage.STATIC
)
private val SPDX_SCOPE_RELATIONSHIPS = SpdxRelationship.Type.entries.filter { it.name.endsWith("_DEPENDENCY_OF") }
data class SpdxDocumentFileConfig(
/**
* If this option is enabled and an SPDX package has a PURL as an external reference, the ORT [Package]'s
* [Identifier] is deduced from that PURL instead of from the [SpdxPackage]'s [ID][SpdxPackage.spdxId].
*/
@OrtPluginOption(defaultValue = "false")
val deduceOrtIdFromPurl: Boolean
)
/**
* A "fake" package manager implementation that uses SPDX documents as definition files to declare projects and describe
* packages. See https://github.com/spdx/spdx-spec/issues/439 for details.
*/
@OrtPlugin(
displayName = "SPDX Document File",
summary = "A package manager that uses SPDX documents as definition files.",
factory = PackageManagerFactory::class
)
class SpdxDocumentFile(
override val descriptor: PluginDescriptor = SpdxDocumentFileFactory.descriptor,
private val config: SpdxDocumentFileConfig
) :
PackageManager(PROJECT_TYPE) {
override val globsForDefinitionFiles = listOf("*.spdx.yml", "*.spdx.yaml", "*.spdx.json")
private val spdxDocumentCache = SpdxDocumentCache()
/**
* Return the dependencies of the package with the given [pkgId] defined in [doc] of the
* [SpdxRelationship.Type.DEPENDENCY_OF] type. Identified dependencies are mapped to ORT [Package]s and then
* added to [packages]. The [ancestorIds] set contains the IDs of the package that have already been encountered;
* it is used to detect circular dependencies.
*/
private fun getDependencies(
pkgId: String,
doc: SpdxResolvedDocument,
packages: MutableSet<Package>,
ancestorIds: MutableSet<String>,
analyzerConfig: AnalyzerConfiguration
): Set<PackageReference> {
logger.debug { "Retrieving dependencies for package '$pkgId'." }
if (!ancestorIds.add(pkgId)) {
logger.warn { "A cycle was detected in the dependencies for packages $ancestorIds." }
return emptySet()
}
return getDependencies(
pkgId,
doc,
packages,
ancestorIds,
SpdxRelationship.Type.DEPENDENCY_OF,
analyzerConfig
) { target ->
val issues = mutableListOf<Issue>()
getPackageManagerDependency(target, doc, analyzerConfig) ?: doc.getSpdxPackageForId(target, issues)
?.let { dependency ->
val targetFile = doc.getDefinitionFile(target)
val ortPackage = dependency.toPackage(targetFile, doc, config.deduceOrtIdFromPurl)
packages += ortPackage
PackageReference(
id = ortPackage.id,
dependencies = getDependencies(target, doc, packages, ancestorIds, analyzerConfig),
linkage = getLinkageForDependency(dependency, pkgId, doc.relationships),
issues = issues
)
}
}.also { ancestorIds.remove(pkgId) }
}
/**
* Return the dependencies of the package with the given [pkgId] defined in [doc] of the given
* [dependencyOfRelation] type. Optionally, the [SpdxRelationship.Type.DEPENDS_ON] type is handled by
* [dependsOnCase]. Identified dependencies are mapped to ORT [Package]s and then added to [packages].
* Use the given [ancestorIds] set to detect cyclic dependencies.
*/
private fun getDependencies(
pkgId: String,
doc: SpdxResolvedDocument,
packages: MutableSet<Package>,
ancestorIds: MutableSet<String>,
dependencyOfRelation: SpdxRelationship.Type,
analyzerConfig: AnalyzerConfiguration,
dependsOnCase: (String) -> PackageReference? = { null }
): Set<PackageReference> =
doc.relationships.mapNotNullTo(mutableSetOf()) { (source, relation, target, _) ->
val issues = mutableListOf<Issue>()
val isDependsOnRelation = relation == SpdxRelationship.Type.DEPENDS_ON || hasDefaultScopeLinkage(
source, target, relation, doc.relationships
)
when {
// Dependencies can either be defined on the target...
pkgId.equals(target, ignoreCase = true) && relation == dependencyOfRelation -> {
if (pkgId != target) {
issues += createAndLogIssue("Source '$pkgId' has to match target '$target' case-sensitively.")
}
getPackageManagerDependency(source, doc, analyzerConfig) ?: doc.getSpdxPackageForId(source, issues)
?.let { dependency ->
val sourceFile = doc.getDefinitionFile(source)
val ortPackage = dependency.toPackage(sourceFile, doc, config.deduceOrtIdFromPurl)
packages += ortPackage
PackageReference(
id = ortPackage.id,
dependencies = getDependencies(source, doc, packages, ancestorIds, analyzerConfig),
issues = issues,
linkage = getLinkageForDependency(dependency, target, doc.relationships)
)
}
}
// ...or on the source.
pkgId.equals(source, ignoreCase = true) && isDependsOnRelation -> {
if (pkgId != source) {
issues += createAndLogIssue("Source '$source' has to match target '$pkgId' case-sensitively.")
}
val pkgRef = dependsOnCase(target)
pkgRef?.copy(issues = issues + pkgRef.issues)
}
else -> null
}
}
/**
* Return a [Scope] created from the given type of [relation] for the [projectPackage][projectPackageId] in
* [spdxDocument], or `null` if there are no such relations. Identified dependencies are mapped to ORT [Package]s
* and then added to [packages].
*/
private fun createScope(
spdxDocument: SpdxResolvedDocument,
projectPackageId: String,
relation: SpdxRelationship.Type,
packages: MutableSet<Package>,
analyzerConfig: AnalyzerConfiguration
): Scope? =
getDependencies(projectPackageId, spdxDocument, packages, mutableSetOf(), relation, analyzerConfig).takeUnless {
it.isEmpty()
}?.let {
Scope(
name = relation.name.removeSuffix("_DEPENDENCY_OF").lowercase(),
dependencies = it
)
}
override fun mapDefinitionFiles(
analysisRoot: File,
definitionFiles: List<File>,
analyzerConfig: AnalyzerConfiguration
): List<File> =
definitionFiles.associateWith {
spdxDocumentCache.load(it).getOrNull()
}.filter { (_, spdxDocument) ->
// Distinguish whether we have a project-style SPDX document that describes a project and its dependencies,
// or a package-style SPDX document that describes a single (dependency-)package.
spdxDocument?.projectPackage() != null
}.keys.also { remainingFiles ->
if (remainingFiles.isEmpty()) return definitionFiles
val discardedFiles = definitionFiles - remainingFiles
if (discardedFiles.isNotEmpty()) {
logger.info {
"Discarded the following ${discardedFiles.size} non-project SPDX files: " +
discardedFiles.joinToString { "'$it'" }
}
}
}.toList()
override fun resolveDependencies(
analysisRoot: File,
definitionFile: File,
excludes: Excludes,
includes: Includes,
analyzerConfig: AnalyzerConfiguration,
labels: Map<String, String>
): List<ProjectAnalyzerResult> {
val transitiveDocument = SpdxResolvedDocument.load(spdxDocumentCache, definitionFile)
val spdxDocument = transitiveDocument.rootDocument.document
val packages = mutableSetOf<Package>()
val scopes = mutableSetOf<Scope>()
val projectPackage = requireNotNull(spdxDocument.projectPackage() ?: spdxDocument.packages.firstOrNull()) {
"The SPDX document file at '$definitionFile' does not describe a project."
}
logger.info {
"File '$definitionFile' contains SPDX document '${spdxDocument.name}' which describes project " +
"'${projectPackage.name}'."
}
SPDX_SCOPE_RELATIONSHIPS.mapNotNullTo(scopes) { type ->
createScope(transitiveDocument, projectPackage.spdxId, type, packages, analyzerConfig)
}
scopes += Scope(
name = DEFAULT_SCOPE_NAME,
dependencies = getDependencies(
projectPackage.spdxId,
transitiveDocument,
packages,
mutableSetOf(),
analyzerConfig
)
)
val purlReference = projectPackage.locateExternalReference(SpdxExternalReference.Type.Purl)
val id = purlReference?.takeIf { config.deduceOrtIdFromPurl }?.run { toPackageUrl()?.toIdentifier() }
?: projectPackage.toIdentifier(projectType)
val project = Project(
id = id,
cpe = projectPackage.locateCpe(),
definitionFilePath = VersionControlSystem.getPathInfo(definitionFile).path,
authors = projectPackage.originator.wrapPresentInSet(),
declaredLicenses = setOf(projectPackage.licenseDeclared),
vcs = processProjectVcs(definitionFile.parentFile, VcsInfo.EMPTY),
homepageUrl = projectPackage.homepage.mapNotPresentToEmpty(),
scopeDependencies = scopes
)
return listOf(ProjectAnalyzerResult(project, packages, transitiveDocument.getIssuesWithoutSpdxPackage()))
}
/**
* Create the final [PackageManagerResult] by making sure that packages are removed from [projectResults] that
* are also referenced as project dependencies.
*/
override fun createPackageManagerResult(
projectResults: Map<File, List<ProjectAnalyzerResult>>
): PackageManagerResult = PackageManagerResult(projectResults.filterProjectPackages())
}
internal fun getPackageManagerDependency(
pkgId: String,
doc: SpdxResolvedDocument,
analyzerConfig: AnalyzerConfiguration
): PackageReference? {
val issues = mutableListOf<Issue>()
val spdxPackage = doc.getSpdxPackageForId(pkgId, issues) ?: return null
val definitionFile = doc.getDefinitionFile(pkgId) ?: return null
if (spdxPackage.packageFilename.isBlank()) return null
val scope = spdxPackage.extractScopeFromExternalReferences() ?: return null
val packageFile = definitionFile.resolveSibling(spdxPackage.packageFilename)
if (packageFile.isFile) {
val managedFiles = PackageManager.findManagedFiles(
packageFile.parentFile,
analyzerConfig.determineEnabledPackageManagers().map {
val options = analyzerConfig.getPackageManagerConfiguration(it.descriptor.id)?.options.orEmpty()
it.create(PluginConfig(options))
}
)
managedFiles.forEach { (manager, files) ->
if (files.any { it.canonicalPath == packageFile.canonicalPath }) {
// TODO: The data from the spdxPackage is currently ignored, check if some fields need to be
// preserved somehow.
return PackageManagerDependency(
packageManager = manager.descriptor.id,
definitionFile = VersionControlSystem.getPathInfo(packageFile).path,
scope = scope,
linkage = PackageLinkage.PROJECT_STATIC // TODO: Set linkage based on SPDX reference type.
).toPackageReference(issues)
}
}
}
return null
}
/**
* Return the [PackageLinkage] between [dependency] and [dependant] as specified in [relationships]. If no
* relationship is found, return [PackageLinkage.DYNAMIC].
*/
private fun getLinkageForDependency(
dependency: SpdxPackage,
dependant: String,
relationships: List<SpdxRelationship>
): PackageLinkage =
relationships.mapNotNull { relation ->
SPDX_LINKAGE_RELATIONSHIPS[relation.relationshipType]?.takeIf {
val relationId = if (relation.relatedSpdxElement.isExternalDocumentReferenceId()) {
relation.relatedSpdxElement.substringAfter(":")
} else {
relation.relatedSpdxElement
}
relationId == dependency.spdxId && relation.spdxElementId == dependant
}
}.singleOrNull() ?: PackageLinkage.DYNAMIC
/**
* Return true if the [relation] as defined in [relationships] describes an [SPDX_LINKAGE_RELATIONSHIPS] in the
* [DEFAULT_SCOPE_NAME] so that the [source] depends on the [target].
*/
private fun hasDefaultScopeLinkage(
source: String,
target: String,
relation: SpdxRelationship.Type,
relationships: List<SpdxRelationship>
): Boolean {
if (relation !in SPDX_LINKAGE_RELATIONSHIPS) return false
val hasScopeRelationship = relationships.any {
it.relationshipType in SPDX_SCOPE_RELATIONSHIPS
// Scope relationships are defined in "reverse" as a "dependency of".
&& it.relatedSpdxElement == source && it.spdxElementId == target
}
return !hasScopeRelationship
}