Skip to content

Commit a8ccf27

Browse files
feat(scanner): Add Nomos plugin for license scanning
Signed-off-by: Prakash Mishra <[email protected]> chore(scanner): Update display name, fix detekt issue, and add copyright Signed-off-by: Prakash Mishra <[email protected]>
1 parent 78fb251 commit a8ccf27

File tree

6 files changed

+353
-0
lines changed

6 files changed

+353
-0
lines changed

NOTICE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ Copyright (C) 2023-2024 Double Open Oy
1818
Copyright (C) 2024 Robert Bosch GmbH
1919
Copyright (C) 2024 Cariad SE
2020
Copyright (C) 2025 Quartett mobile GmbH
21+
Copyright (C) 2025 Prakash Mishra
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
plugins {
21+
// Apply precompiled plugins.
22+
id("ort-plugin-conventions")
23+
24+
// Apply third-party plugins.
25+
alias(libs.plugins.kotlinSerialization)
26+
}
27+
28+
dependencies {
29+
api(projects.model)
30+
api(projects.scanner)
31+
32+
implementation(projects.utils.commonUtils)
33+
implementation(projects.utils.ortUtils)
34+
implementation(projects.utils.spdxUtils)
35+
36+
implementation(libs.kotlinx.serialization.json)
37+
38+
ksp(projects.scanner)
39+
40+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.scanners.fossologynomossa
21+
22+
import java.io.File
23+
import java.time.Instant
24+
25+
import kotlin.math.max
26+
27+
import org.apache.logging.log4j.kotlin.logger
28+
29+
import org.ossreviewtoolkit.model.ScanSummary
30+
import org.ossreviewtoolkit.plugins.api.OrtPlugin
31+
import org.ossreviewtoolkit.plugins.api.PluginDescriptor
32+
import org.ossreviewtoolkit.scanner.LocalPathScannerWrapper
33+
import org.ossreviewtoolkit.scanner.ScanContext
34+
import org.ossreviewtoolkit.scanner.ScannerMatcher
35+
import org.ossreviewtoolkit.scanner.ScannerWrapperFactory
36+
import org.ossreviewtoolkit.utils.common.CommandLineTool
37+
import org.ossreviewtoolkit.utils.common.ProcessCapture
38+
39+
object NomossaCommand : CommandLineTool {
40+
override fun command(workingDir: File?) =
41+
listOfNotNull(workingDir, "FOSSology-nomossa").joinToString(File.separator)
42+
43+
override fun transformVersion(output: String) =
44+
// Example output:
45+
// nomos build version: 4.5.1.1 r(ff4fa7)
46+
output.removePrefix("nomos build version: ").substringBefore(' ') // Returns 4.5.1
47+
48+
override fun getVersionArguments() = "-V"
49+
}
50+
51+
/**
52+
* A wrapper for [Nomossa](https://github.com/fossology/fossology/tree/master/src/nomos).
53+
*
54+
* This plugin integrates FOSSology's Nomossa scanner into ORT by calling its CLI
55+
* and mapping its output to ORT's scan result format.
56+
*/
57+
@OrtPlugin(
58+
id = "Nomossa",
59+
displayName = "Nomossa (FOSSology)",
60+
description = "A wrapper for [Nomossa](https://github.com/fossology/fossology/tree/master/src/nomos).",
61+
factory = ScannerWrapperFactory::class
62+
)
63+
class Nomossa(
64+
override val descriptor: PluginDescriptor = NomossaFactory.descriptor,
65+
private val config: NomossaConfig
66+
) : LocalPathScannerWrapper() {
67+
private val commandLineOptions by lazy { getCommandLineOptions() }
68+
69+
internal fun getCommandLineOptions() =
70+
buildList {
71+
addAll(config.additionalOptions)
72+
add("-n")
73+
add(max(2, Runtime.getRuntime().availableProcessors() - 1).toString())
74+
}
75+
76+
override val configuration by lazy {
77+
config.additionalOptions.joinToString(" ")
78+
}
79+
80+
override val matcher by lazy { ScannerMatcher.create(details, config) }
81+
82+
override val version by lazy {
83+
require(NomossaCommand.isInPath()) {
84+
"The '${NomossaCommand.command()}' command is not available in the PATH environment."
85+
}
86+
87+
NomossaCommand.getVersion()
88+
}
89+
90+
override val readFromStorage = config.readFromStorage
91+
override val writeToStorage = config.writeToStorage
92+
93+
override fun runScanner(path: File, context: ScanContext): String {
94+
val process = runNomossa(path)
95+
96+
return with(process) {
97+
if (isError && stdout.isNotBlank()) logger.debug { stdout }
98+
if (stderr.isNotBlank()) logger.debug { stderr }
99+
100+
stdout
101+
}
102+
}
103+
104+
override fun createSummary(result: String, startTime: Instant, endTime: Instant): ScanSummary =
105+
parseNomossaResult(result).toScanSummary(startTime, endTime)
106+
107+
/**
108+
* Execute Nomossa with the configured arguments to scan the given [path].
109+
*/
110+
internal fun runNomossa(path: File): ProcessCapture =
111+
ProcessCapture(
112+
NomossaCommand.command(),
113+
*commandLineOptions.toTypedArray(),
114+
"-d", path.absolutePath // Scan this directory
115+
)
116+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.scanners.fossologynomossa
21+
22+
import org.ossreviewtoolkit.plugins.api.OrtPluginOption
23+
import org.ossreviewtoolkit.scanner.ScannerMatcherCriteria
24+
25+
/**
26+
* Configuration options for the Nomossa scanner.
27+
*/
28+
data class NomossaConfig(
29+
/**
30+
* Command line options that affect scan results. These are used when matching stored results.
31+
*/
32+
@OrtPluginOption(defaultValue = "-J,-S,-l")
33+
val additionalOptions: List<String>,
34+
35+
/**
36+
* The scanner name pattern used when looking up scan results from storage.
37+
*/
38+
override val regScannerName: String?,
39+
40+
/**
41+
* The minimum version of scan results to use from storage.
42+
*/
43+
override val minVersion: String?,
44+
45+
/**
46+
* The maximum version of scan results to use from storage.
47+
*/
48+
override val maxVersion: String?,
49+
50+
/**
51+
* The configuration string for identifying matching scan results in storage.
52+
*/
53+
override val configuration: String?,
54+
55+
/**
56+
* Whether to read scan results from storage.
57+
*/
58+
@OrtPluginOption(defaultValue = "false")
59+
val readFromStorage: Boolean,
60+
61+
/**
62+
* Whether to write scan results to storage.
63+
*/
64+
@OrtPluginOption(defaultValue = "false")
65+
val writeToStorage: Boolean
66+
) : ScannerMatcherCriteria
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.scanners.fossologynomossa
21+
22+
import java.io.File
23+
import java.time.Instant
24+
25+
import org.ossreviewtoolkit.model.LicenseFinding
26+
import org.ossreviewtoolkit.model.ScanSummary
27+
import org.ossreviewtoolkit.model.TextLocation
28+
import org.ossreviewtoolkit.utils.spdx.SpdxConstants
29+
import org.ossreviewtoolkit.utils.spdx.SpdxExpression
30+
31+
internal fun NomossaResult.toScanSummary(startTime: Instant, endTime: Instant): ScanSummary {
32+
val licenseFindings = results.flatMap { fileResult ->
33+
val fileContent = File(fileResult.file).readText()
34+
35+
fileResult.licenses.map { licenseInfo ->
36+
val licenseExpression = runCatching { SpdxExpression.parse(licenseInfo.license) }.getOrNull()
37+
38+
val safeLicense = when {
39+
licenseExpression == null -> SpdxConstants.NOASSERTION
40+
licenseExpression.isValid() -> licenseInfo.license
41+
else -> "LicenseRef-Nomossa-${licenseInfo.license.replace(Regex("[^A-Za-z0-9.+-]"), "-")}"
42+
}
43+
val (startLine, endLine) = byteOffsetsToLineNumbers(fileContent, licenseInfo.start, licenseInfo.end)
44+
45+
LicenseFinding(
46+
license = safeLicense,
47+
location = TextLocation(
48+
path = fileResult.file,
49+
startLine = startLine,
50+
endLine = endLine
51+
)
52+
)
53+
}
54+
}.toSet()
55+
56+
return ScanSummary(
57+
startTime = startTime,
58+
endTime = endTime,
59+
licenseFindings = licenseFindings,
60+
issues = emptyList(),
61+
copyrightFindings = sortedSetOf()
62+
)
63+
}
64+
65+
internal fun byteOffsetsToLineNumbers(fileContent: String, startOffset: Int, endOffset: Int): Pair<Int, Int> {
66+
var startLine = 1
67+
var endLine = 1
68+
69+
for ((index, char) in fileContent.withIndex()) {
70+
if (index >= startOffset && index >= endOffset) break
71+
72+
if (char == '\n') {
73+
if (index < startOffset) startLine++
74+
if (index < endOffset) endLine++
75+
}
76+
}
77+
78+
return Pair(startLine, endLine)
79+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.scanners.fossologynomossa
21+
22+
import kotlinx.serialization.Serializable
23+
import kotlinx.serialization.json.Json
24+
25+
@Serializable
26+
internal data class NomossaResult(
27+
val results: List<NomossaFileResult>
28+
)
29+
30+
@Serializable
31+
internal data class NomossaFileResult(
32+
val file: String,
33+
val licenses: List<NomossaLicenseInfo>
34+
)
35+
36+
@Serializable
37+
internal data class NomossaLicenseInfo(
38+
val license: String,
39+
val start: Int,
40+
val end: Int,
41+
val len: Int
42+
)
43+
44+
private val json = Json {
45+
ignoreUnknownKeys = true
46+
}
47+
48+
/**
49+
* Parses the JSON result string returned by Nomossa into a [NomossaResult] object.
50+
*/
51+
internal fun parseNomossaResult(result: String): NomossaResult = json.decodeFromString(result)

0 commit comments

Comments
 (0)