Skip to content

Commit 0bb3460

Browse files
feat(scanner): Add Nomos plugin for license scanning
Signed-off-by: Prakash Mishra <prakashmishra9921@gmail.com> chore(scanner): Update display name, fix detekt issue, and add copyright Signed-off-by: Prakash Mishra <prakashmishra9921@gmail.com>
1 parent b1a1450 commit 0bb3460

File tree

6 files changed

+347
-0
lines changed

6 files changed

+347
-0
lines changed

NOTICE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ Copyright (C) 2022-2024 EPAM Systems, Inc.
1717
Copyright (C) 2023-2024 Double Open Oy
1818
Copyright (C) 2024 Robert Bosch GmbH
1919
Copyright (C) 2024 Cariad SE
20+
Copyright (C) 2025 Prakash Mishra
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (C) 2025 Prakash Mishra
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+
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.22"
22+
id("ort-plugin-conventions")
23+
}
24+
25+
dependencies {
26+
api(projects.model)
27+
api(projects.scanner)
28+
29+
implementation(projects.utils.commonUtils)
30+
31+
implementation(projects.utils.ortUtils)
32+
implementation(projects.utils.spdxUtils)
33+
34+
ksp(projects.scanner)
35+
36+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
37+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright (C) 2025 Prakash Mishra
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.apache.logging.log4j.kotlin.logger
26+
27+
import org.ossreviewtoolkit.model.ScanSummary
28+
import org.ossreviewtoolkit.model.ScannerDetails
29+
import org.ossreviewtoolkit.plugins.api.OrtPlugin
30+
import org.ossreviewtoolkit.plugins.api.PluginDescriptor
31+
import org.ossreviewtoolkit.scanner.LocalPathScannerWrapper
32+
import org.ossreviewtoolkit.scanner.ScanContext
33+
import org.ossreviewtoolkit.scanner.ScannerMatcher
34+
import org.ossreviewtoolkit.scanner.ScannerWrapperFactory
35+
import org.ossreviewtoolkit.utils.common.CommandLineTool
36+
import org.ossreviewtoolkit.utils.common.ProcessCapture
37+
38+
object NomossaCommand : CommandLineTool {
39+
override fun command(workingDir: File?): String {
40+
return listOfNotNull(workingDir, "nomossa").joinToString(File.separator)
41+
}
42+
43+
override fun transformVersion(output: String): String {
44+
// Example output: nomossasa build version: 4.5.1.1 r(ff4fa7)
45+
val versionRegex = Regex("""(\d+\.\d+\.\d+)(?:\.\d+)?""")
46+
return versionRegex.find(output)?.groupValues?.get(1).orEmpty() // Returns 4.5.1
47+
}
48+
49+
override fun getVersionArguments() = "-V"
50+
}
51+
52+
/**
53+
* A wrapper for [Nomossa](https://github.com/fossology/fossology).
54+
*
55+
* This plugin integrates FOSSology's Nomossa scanner into ORT by calling its CLI
56+
* and mapping its output to ORT's scan result format.
57+
*/
58+
@OrtPlugin(
59+
id = "FOSSology-Nomossa",
60+
displayName = "FOSSology-Nomossa",
61+
description = "A wrapper for [Nomossa](https://github.com/fossology/fossology).",
62+
factory = ScannerWrapperFactory::class
63+
)
64+
class Nomossa(
65+
override val descriptor: PluginDescriptor = NomossaFactory.descriptor,
66+
private val config: NomossaConfig
67+
) : LocalPathScannerWrapper() {
68+
69+
private val commandLineOptions by lazy { getCommandLineOptions() }
70+
71+
internal fun getCommandLineOptions(): List<String> {
72+
val options = LinkedHashSet(config.additionalOptions)
73+
options.add("-J")
74+
options.add("-l")
75+
options.add("-n")
76+
options.add(config.cpuCount.toString())
77+
return options.toList()
78+
}
79+
80+
override val configuration by lazy {
81+
config.additionalOptions.joinToString(" ")
82+
}
83+
84+
override val matcher by lazy { ScannerMatcher.create(details, config) }
85+
86+
override val version by lazy {
87+
require(NomossaCommand.isInPath()) {
88+
"The '${NomossaCommand.command()}' command is not available in the PATH environment."
89+
}
90+
91+
NomossaCommand.getVersion()
92+
}
93+
94+
override val readFromStorage = config.readFromStorage
95+
override val writeToStorage = config.writeToStorage
96+
97+
override fun runScanner(path: File, context: ScanContext): String {
98+
val process = runNomossa(path)
99+
100+
val resultText = process.stdout
101+
logger.info { "Nomossa raw output:\n$resultText" }
102+
103+
return with(process) {
104+
if (isError && stdout.isNotBlank()) logger.debug { stdout }
105+
if (stderr.isNotBlank()) logger.debug { stderr }
106+
107+
stdout
108+
}
109+
}
110+
111+
override fun parseDetails(result: String): ScannerDetails {
112+
return ScannerDetails(
113+
name = descriptor.id,
114+
version = version,
115+
configuration = config.additionalOptions.joinToString(" ")
116+
)
117+
}
118+
119+
override fun createSummary(result: String, startTime: Instant, endTime: Instant): ScanSummary =
120+
parseNomossaResult(result).toScanSummary(startTime, endTime)
121+
122+
/**
123+
* Execute Nomossa with the configured arguments to scan the given [path].
124+
*/
125+
internal fun runNomossa(path: File): ProcessCapture =
126+
ProcessCapture(
127+
NomossaCommand.command(),
128+
*commandLineOptions.toTypedArray(),
129+
"-d", path.absolutePath // Scan this directory
130+
)
131+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright (C) 2025 Prakash Mishra
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,-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 {
67+
68+
/**
69+
* Number of CPU processes Nomossa should use.
70+
*/
71+
@OrtPluginOption(defaultValue = "2")
72+
val cpuCount: Int = 2
73+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright (C) 2025 Prakash Mishra
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.time.Instant
23+
24+
import org.ossreviewtoolkit.model.LicenseFinding
25+
import org.ossreviewtoolkit.model.ScanSummary
26+
import org.ossreviewtoolkit.model.TextLocation
27+
28+
internal fun NomossaResult.toScanSummary(startTime: Instant, endTime: Instant): ScanSummary {
29+
val licenseFindings = results.flatMap { fileResult ->
30+
fileResult.licenses.map { rawLicense ->
31+
32+
val safeLicense = if (rawLicense.matches(Regex("^[A-Za-z0-9.\\-+]+$"))) {
33+
rawLicense
34+
} else {
35+
"LicenseRef-Nomossa-${rawLicense.replace(Regex("[^A-Za-z0-9.+-]"), "-")}" // License not found
36+
}
37+
38+
LicenseFinding(
39+
license = safeLicense, // use raw string
40+
location = TextLocation(
41+
path = fileResult.file,
42+
startLine = 1,
43+
endLine = 1
44+
)
45+
)
46+
}
47+
}.toSortedSet(
48+
compareBy(
49+
{ it.license.toString() },
50+
{ it.location.path },
51+
{ it.location.startLine }
52+
)
53+
)
54+
55+
return ScanSummary(
56+
startTime = startTime,
57+
endTime = endTime,
58+
licenseFindings = licenseFindings,
59+
issues = emptyList(), // no SPDX parsing issues anymore
60+
copyrightFindings = sortedSetOf()
61+
)
62+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (C) 2025 Prakash Mishra
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<String>
34+
)
35+
36+
private val json = Json {
37+
ignoreUnknownKeys = true
38+
}
39+
40+
/**
41+
* Parses the JSON result string returned by Nomossa into a [NomossaResult] object.
42+
*/
43+
internal fun parseNomossaResult(result: String): NomossaResult = json.decodeFromString(result)

0 commit comments

Comments
 (0)