diff --git a/build.gradle.kts b/build.gradle.kts index 1151e976d..b6f89d5ba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -78,3 +78,13 @@ subprojects { } } } + +/* + * Optional and experimental features + */ +// this code block also exists in `settings.gradle.kts` +val enablePluginSupport: Boolean by extra { + val enablePluginSupport: String? by project + enablePluginSupport.toBoolean() +} +project.logger.lifecycle("Plugin feature is ${if (enablePluginSupport) "enabled" else "disabled"}") diff --git a/buildSrc/src/main/kotlin/features.gradle.kts b/buildSrc/src/main/kotlin/features.gradle.kts new file mode 100644 index 000000000..e9ccfbed8 --- /dev/null +++ b/buildSrc/src/main/kotlin/features.gradle.kts @@ -0,0 +1,9 @@ +plugins { + kotlin("jvm") +} + +val enablePluginSupport: Boolean by rootProject.extra + +dependencies { + if (enablePluginSupport) runtimeOnly(project(":codyze-plugins")) +} \ No newline at end of file diff --git a/code-coverage-report/build.gradle.kts b/code-coverage-report/build.gradle.kts index a1bff7c6e..b8fe7d4bb 100644 --- a/code-coverage-report/build.gradle.kts +++ b/code-coverage-report/build.gradle.kts @@ -33,12 +33,18 @@ reporting { } } +val enablePluginSupport: Boolean by rootProject.extra +project.logger.lifecycle("Plugin feature is ${if (enablePluginSupport) "enabled" else "disabled"}") + dependencies { jacocoAggregation(projects.codyzeBackends.cpg) jacocoAggregation(projects.codyzeCli) jacocoAggregation(projects.codyzeCore) jacocoAggregation(projects.codyzeSpecificationLanguages.coko.cokoCore) jacocoAggregation(projects.codyzeSpecificationLanguages.coko.cokoDsl) + + // Optional and experimental features + if (enablePluginSupport) jacocoAggregation(project(":codyze-plugins")) } tasks.check { diff --git a/codecov.yml b/codecov.yml index 34f4f4aa8..2d561c9d2 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,3 +1,6 @@ +ignore: + - "**/cli/Main.kt" + - "**/codyze/core/plugin/*.kt" coverage: status: project: diff --git a/codyze-cli/build.gradle.kts b/codyze-cli/build.gradle.kts index fd75af525..2a401f363 100644 --- a/codyze-cli/build.gradle.kts +++ b/codyze-cli/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("documented-module") + id("features") application } diff --git a/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/CodyzeCli.kt b/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/CodyzeCli.kt index 17b6950de..ef730141f 100644 --- a/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/CodyzeCli.kt +++ b/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/CodyzeCli.kt @@ -18,6 +18,7 @@ package de.fraunhofer.aisec.codyze.cli import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.NoOpCliktCommand import com.github.ajalt.clikt.core.context +import com.github.ajalt.clikt.core.findOrSetObject import com.github.ajalt.clikt.output.MordantHelpFormatter import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.multiple @@ -27,6 +28,7 @@ import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.versionOption import com.github.ajalt.clikt.parameters.types.path import de.fraunhofer.aisec.codyze.core.VersionProvider +import de.fraunhofer.aisec.codyze.core.executor.ExecutorCommand import java.nio.file.Path import kotlin.io.path.Path @@ -68,7 +70,11 @@ class ConfigFileParser : CliktCommand(treatUnknownOptionsAsArgs = true) { */ @Suppress("Unused", "UnusedPrivateMember") class CodyzeCli(val configFile: Path?) : - NoOpCliktCommand(help = "Codyze finds security flaws in source code", printHelpOnEmptyArgs = true) { + NoOpCliktCommand( + help = "Codyze finds security flaws in source code", + printHelpOnEmptyArgs = true, + allowMultipleSubcommands = true + ) { init { versionOption( @@ -86,4 +92,11 @@ class CodyzeCli(val configFile: Path?) : private val unusedConfigFile: Path? by configFileOption() val codyzeOptions by CodyzeOptionGroup() + + val usedExecutors by findOrSetObject { mutableListOf>() } + + // This run method is only necessary to correctly set the "usedExecutors" variable + override fun run() { + usedExecutors + } } diff --git a/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/KoinModules.kt b/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/KoinModules.kt index dd72e0152..12386ccad 100644 --- a/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/KoinModules.kt +++ b/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/KoinModules.kt @@ -23,10 +23,12 @@ import de.fraunhofer.aisec.codyze.core.executor.Executor import de.fraunhofer.aisec.codyze.core.executor.ExecutorCommand import de.fraunhofer.aisec.codyze.core.output.OutputBuilder import de.fraunhofer.aisec.codyze.core.output.SarifBuilder +import de.fraunhofer.aisec.codyze.core.plugin.Plugin import de.fraunhofer.aisec.codyze.specificationLanguages.coko.dsl.cli.CokoSubcommand import org.koin.core.module.dsl.factoryOf import org.koin.dsl.bind import org.koin.dsl.module +import java.util.ServiceLoader /** * Every [Backend] must provide [BackendCommand] to be selectable in the CLI. @@ -49,3 +51,8 @@ val executorCommands = module { val outputBuilders = module { factoryOf(::SarifBuilder) bind(OutputBuilder::class) } + +/** + * List all available [Plugin]s. They use external tools to extend the analysis. + */ +val plugins = ServiceLoader.load(Plugin::class.java).map { it.module() } diff --git a/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/Main.kt b/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/Main.kt index 10a4689d3..040e3a464 100644 --- a/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/Main.kt +++ b/codyze-cli/src/main/kotlin/de/fraunhofer/aisec/codyze/cli/Main.kt @@ -18,20 +18,19 @@ package de.fraunhofer.aisec.codyze.cli import com.github.ajalt.clikt.core.subcommands import de.fraunhofer.aisec.codyze.core.backend.BackendCommand import de.fraunhofer.aisec.codyze.core.executor.ExecutorCommand -import io.github.oshai.kotlinlogging.KotlinLogging +import de.fraunhofer.aisec.codyze.core.output.aggregator.Aggregate +import de.fraunhofer.aisec.codyze.core.plugin.Plugin import org.koin.core.context.startKoin import org.koin.java.KoinJavaComponent.getKoin import java.nio.file.Path -private val logger = KotlinLogging.logger {} - /** Entry point for Codyze. */ fun main(args: Array) { startKoin { // Initialize the koin dependency injection // use Koin logger printLogger() // declare modules - modules(executorCommands, backendCommands, outputBuilders) + modules(listOf(executorCommands, backendCommands, outputBuilders) + plugins) } // parse the CMD arguments @@ -44,26 +43,31 @@ fun main(args: Array) { } finally { // parse the arguments based on the codyze options and the executorOptions/backendOptions codyzeCli = CodyzeCli(configFile = configFile) - codyzeCli.subcommands(getKoin().getAll>()) + codyzeCli.subcommands(getKoin().getAll>() + getKoin().getAll()) codyzeCli.main(args) } - - // get the used subcommands - val executorCommand = codyzeCli.currentContext.invokedSubcommand as? ExecutorCommand<*> - - // allow backendCommand to be null in order to allow executors that do not use backends - val backendCommand = executorCommand?.currentContext?.invokedSubcommand as? BackendCommand<*> + // Following code will be executed after all commands' "run" functions complete // this should already be checked by clikt in [codyzeCli.main(args)] - requireNotNull(executorCommand) { "UsageError! Please select one of the available executors." } + require(codyzeCli.usedExecutors.isNotEmpty()) { "UsageError! Please select one of the available executors." } + for (executorCommand in codyzeCli.usedExecutors) { + // allow backendCommand to be null in order to allow executors that do not use backends + val backendCommand = executorCommand.currentContext.invokedSubcommand as? BackendCommand<*> - val codyzeConfiguration = codyzeCli.codyzeOptions.asConfiguration() - // the subcommands know how to instantiate their respective backend/executor - val backend = backendCommand?.getBackend() // [null] if the chosen executor does not support modular backends - val executor = executorCommand.getExecutor(codyzeConfiguration.goodFindings, codyzeConfiguration.pedantic, backend) + val codyzeConfiguration = codyzeCli.codyzeOptions.asConfiguration() - val run = executor.evaluate() + // the subcommands know how to instantiate their respective backend/executor + val backend = backendCommand?.getBackend() // [null] if the chosen executor does not support modular backends + val executor = executorCommand.getExecutor( + codyzeConfiguration.goodFindings, + codyzeConfiguration.pedantic, + backend + ) - // use the chosen [OutputBuilder] to convert the SARIF format (a SARIF RUN) from the executor to the chosen format - codyzeConfiguration.outputBuilder.toFile(run, codyzeConfiguration.output) + val run = executor.evaluate() + Aggregate.addRun(run) + + // use the chosen OutputBuilder to convert the SARIF format (a SARIF Run) from the executor to the chosen format + codyzeConfiguration.outputBuilder.toFile(Aggregate.createRun() ?: run, codyzeConfiguration.output) + } } diff --git a/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/config/Configuration.kt b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/config/Configuration.kt index 78b0cd2b0..7df1b3579 100644 --- a/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/config/Configuration.kt +++ b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/config/Configuration.kt @@ -16,11 +16,8 @@ package de.fraunhofer.aisec.codyze.core.config import de.fraunhofer.aisec.codyze.core.output.OutputBuilder -import io.github.oshai.kotlinlogging.KotlinLogging import java.nio.file.Path -private val logger = KotlinLogging.logger {} - /** * Holds the main configuration to run Codyze with * diff --git a/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/executor/ExecutorCommand.kt b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/executor/ExecutorCommand.kt index 1f3194700..08822c89a 100644 --- a/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/executor/ExecutorCommand.kt +++ b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/executor/ExecutorCommand.kt @@ -15,8 +15,7 @@ */ package de.fraunhofer.aisec.codyze.core.executor -import com.github.ajalt.clikt.core.NoOpCliktCommand -import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.core.* import de.fraunhofer.aisec.codyze.core.backend.Backend import de.fraunhofer.aisec.codyze.core.backend.BackendCommand import de.fraunhofer.aisec.codyze.core.backend.BackendOptions @@ -29,6 +28,9 @@ import org.koin.java.KoinJavaComponent.getKoin abstract class ExecutorCommand(cliName: String? = null) : NoOpCliktCommand(hidden = true, name = cliName) { + /** Use the global context set in [CodyzeCli] */ + private val usedExecutors by findOrSetObject { mutableListOf>() } + abstract fun getExecutor(goodFindings: Boolean, pedantic: Boolean, backend: Backend?): T /** @@ -39,4 +41,8 @@ abstract class ExecutorCommand(cliName: String? = null) : inline fun registerBackendOptions() { subcommands(getKoin().getAll>().filter { it.backend == T::class }) } + + override fun run() { + usedExecutors += this + } } diff --git a/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/Aggregate.kt b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/Aggregate.kt new file mode 100644 index 000000000..4d15ec0c3 --- /dev/null +++ b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/Aggregate.kt @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.core.output.aggregator + +import io.github.detekt.sarif4k.* +import io.github.oshai.kotlinlogging.KotlinLogging + +private val logger = KotlinLogging.logger { } + +/** + * A static class containing information about an aggregated SARIF run consisting of multiple separate runs. + * Each external Tool will be listed as an extension while Codyze functions as the driver. + * However, each SARIF report will be reduced to fields that are present and handled in this class. + */ +object Aggregate { + // The driver is null iff containedRuns is empty + private var driver: ToolComponent? = null + private var extensions: List = listOf() + private var results: List = listOf() + private var invocations: List = listOf() + private var containedRuns: Set = setOf() + + /** + * Creates a new run from the information stored within the aggregate. + */ + fun createRun(): Run? { + val currentDriver = driver + + // Prevent Exception from uninitialized driver + if (currentDriver == null) { + logger.error { "Failed to create run from aggregate: No driver added yet" } + return null + } + + logger.info { "Creating single run from aggregate consisting of ${containedRuns.size} runs" } + return Run( + tool = Tool(currentDriver, extensions), + results = results, + invocations = listOf(createInvocation()) + ) + } + + /** + * Adds a new run to the aggregate. + * The driver of the first run will be locked in as the driver for the aggregate. + * @param run The new SARIF run to add + * @return The aggregate after adding the run + */ + fun addRun(run: Run) { + val modifiedRun: Run = modifyResults(run) + + val originalDriver = modifiedRun.tool.driver + + // Here we hardcode Codyze as the only possible driver for the aggregate + // otherwise the driver of the aggregate would depend on the order of subcommands + if (driver == null && originalDriver.product == "Codyze") { + driver = originalDriver + } else { + extensions += originalDriver + } + extensions += modifiedRun.tool.extensions.orEmpty() + results += modifiedRun.results.orEmpty() + invocations += modifiedRun.invocations.orEmpty() + containedRuns += modifiedRun + + logger.info { "Added run from ${originalDriver.name} to the aggregate" } + } + + /** + * Resets the information stored within the aggregate + */ + fun reset() { + driver = null + extensions = listOf() + results = listOf() + containedRuns = setOf() + } + + /** + * Modifies the results object in a way that allows unique reference to the corresponding rule object + * within the aggregate. + * For this we need to populate the rule property in each result with the correct toolComponentReference + * or create the property from scratch. + * The properties result.ruleID and result.ruleIndex will be moved into the rule object if they exist. + * @param run The run which should have its results modified + * @return The run after applying the modifications + */ + private fun modifyResults(run: Run): Run { + val newResults = run.results?.map { result -> + var newResult = result.copy(ruleIndex = null, ruleID = null) + val oldRule = result.rule + if (oldRule != null) { + // rule property exists: keep id and index unchanged. + // ToolComponent must be set, otherwise it defaults to driver + val component = oldRule.toolComponent + // here we move result.ruleID and result.ruleIndex into the rule object if necessary + var newRule = oldRule.copy( + id = oldRule.id ?: result.ruleID, + index = oldRule.index ?: result.ruleIndex + ) + if (component != null) { + // reference to component exists: fix index if necessary + val oldIndex = component.index + if (oldIndex != null) { + val newComponent = component.copy(index = oldIndex + 1 + extensions.size) + newRule = newRule.copy(toolComponent = newComponent) + } + newResult = newResult.copy(rule = newRule) + } else { + // no reference to component: create new reference to the old driver (may now be an extension) + val newComponent = ToolComponentReference( + guid = run.tool.driver.guid, + index = if (containedRuns.isEmpty()) null else extensions.size.toLong(), + name = run.tool.driver.name + ) + newRule = newRule.copy(toolComponent = newComponent) + newResult = newResult.copy(rule = newRule) + } + } else { + // rule property does not exist: create property that references the driver (no toolComponent-index) + val driverRules = run.tool.driver.rules + val oldIndex = result.ruleIndex + val rule = if (oldIndex != null) { + driverRules?.get(oldIndex.toInt()) + } else { + driverRules?.firstOrNull { it.id == result.ruleID } + } + + // if no rule information is available at all, we can keep the result object unchanged + if (rule != null) { + val componentReference = ToolComponentReference( + guid = run.tool.driver.guid, + index = null, + name = run.tool.driver.name, + ) + val ruleReference = ReportingDescriptorReference( + guid = rule.guid, + id = result.ruleID, + index = result.ruleIndex, + toolComponent = componentReference + ) + newResult = newResult.copy(rule = ruleReference) + } + } + newResult + } + + return run.copy(results = newResults) + } + + /** + * Creates a new Invocation object from all invocations contained in the aggregate. + * The resulting invocation only indicates a successful execution if all contained invocations do so. + * On failed execution, the resulting invocation indicates which tool failed. + * @return An invocation created from the information in the aggregate + */ + private fun createInvocation(): Invocation { + var executionSuccessful = true + val notifications: MutableList = mutableListOf() + + for (run in containedRuns) { + // First build the toolName with extensions + val toolName = getToolName(run) + // Then create an error message for each failed tool invocation + val unsuccessfulInvocations = run.invocations?.filter { !it.executionSuccessful }.orEmpty() + for (inv in unsuccessfulInvocations) { + executionSuccessful = false + val reason = if (inv.exitCodeDescription != null) " (${inv.exitCodeDescription})" else "" + val message = "Tool $toolName failed execution$reason" + notifications += Notification(level = Level.Error, message = Message(text = message)) + } + } + + // We do not define exitCodeDescription or executionSuccessful based on Codyze + // as it doesn't produce an invocation object + return Invocation(executionSuccessful = executionSuccessful, toolExecutionNotifications = notifications) + } + + /** + * Gets the name of driver + extensions of a run + * @param run + * @return The name in the format "driver (+ ex1, ex2, ...)" + */ + private fun getToolName(run: Run): String { + var toolName = run.tool.driver.name + val extensions = run.tool.extensions + if (extensions != null) { + toolName += " (+ " + for (extension in extensions) { + toolName += extension.name + if (extension != extensions.last()) { + toolName += ", " + } + } + toolName += ")" + } + return toolName + } +} diff --git a/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/Parser.kt b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/Parser.kt new file mode 100644 index 000000000..e505575ca --- /dev/null +++ b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/Parser.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.core.output.aggregator + +import io.github.detekt.sarif4k.Run +import io.github.detekt.sarif4k.SarifSchema210 +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.serialization.SerializationException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import java.io.File +import java.io.IOException + +private val logger = KotlinLogging.logger { } + +/** + * Extracts the last run from a valid SARIF result file + * @param resultFile The file containing the SARIF report + * @return Its last run or null on error + */ +fun extractLastRun(resultFile: File): Run? { + if (!resultFile.exists()) { + logger.error { "The SARIF file at \"${resultFile.canonicalPath}\" does not exist" } + return null + } + + val serializer = Json { + ignoreUnknownKeys = true + } + + return try { + // We do not use sarif4k.SarifSerializer as it does not allow us to ignore unknown fields + // such as arbitrary content of rule.properties + val sarif = serializer.decodeFromString(resultFile.readText()) + sarif.runs.last() + } catch (e: SerializationException) { + logger.error { "Failed to serialize SARIF file at \"${resultFile.canonicalPath}\": ${e.localizedMessage}" } + null + } catch (e: IllegalArgumentException) { + logger.error { "File at \"${resultFile.canonicalPath}\" is not valid SARIF: ${e.localizedMessage}" } + null + } catch (e: IOException) { + logger.error { + "Unexpected error while trying to read file at \"${resultFile.canonicalPath}\": ${e.localizedMessage}" + } + null + } +} diff --git a/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/plugin/Plugin.kt b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/plugin/Plugin.kt new file mode 100644 index 000000000..9acef3960 --- /dev/null +++ b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/plugin/Plugin.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.core.plugin + +import com.github.ajalt.clikt.core.NoOpCliktCommand +import com.github.ajalt.clikt.parameters.groups.provideDelegate +import de.fraunhofer.aisec.codyze.core.output.aggregator.Aggregate +import de.fraunhofer.aisec.codyze.core.output.aggregator.extractLastRun +import io.github.oshai.kotlinlogging.KotlinLogging +import org.koin.core.module.Module +import java.io.File +import java.nio.file.Path + +val logger = KotlinLogging.logger { } + +/** + * Plugins perform a standalone analysis independent of the Codyze Executors. + * They usually use already developed libraries from open-source analysis tools. + * When developing a new Plugin, do not forget to add it to the respective [KoinModules], + * otherwise it will not be selectable in the configuration. + * Also, remember to add a page to docs/plugins. + */ +abstract class Plugin(private val cliName: String) : + NoOpCliktCommand(hidden = true, name = cliName) { + private val options by PluginOptionGroup(cliName) + + /** + * Executes the respective analysis tool. + * @param target The files to be analyzed + * @param context Additional context, plugin-specific + * @param output The location of the results + */ + abstract fun execute(target: List, context: List, output: File) + + abstract fun module(): Module + + /** + * Define two plugins as equal if they are of the same type and therefore have the same CLI name. + * This is necessary to filter out duplicate Plugins when parsing the cli arguments + */ + override fun equals(other: Any?): Boolean { + if (other is Plugin) { + return this.cliName == other.cliName + } + return false + } + + override fun hashCode(): Int { + return cliName.hashCode() + } + + override fun run() { + // Execute the Plugin and print to the specified location + execute( + options.target, + options.context, + options.output + ) + + // Add the run to the Aggregate if not specified to be separate + if (!options.separate) { + val run = extractLastRun(options.output) + if (run != null) { + Aggregate.addRun(run) + } + options.output.delete() + } + } +} diff --git a/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/plugin/PluginOptionGroup.kt b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/plugin/PluginOptionGroup.kt new file mode 100644 index 000000000..7e44f0e52 --- /dev/null +++ b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/core/plugin/PluginOptionGroup.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.core.plugin + +import com.github.ajalt.clikt.parameters.groups.OptionGroup +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.multiple +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.file +import com.github.ajalt.clikt.parameters.types.path +import java.io.File +import java.nio.file.Path + +/** + * Holds the common CLI options for all Plugins. + * Used in e.g., [PMDPlugin] and [FindSecBugsPlugin]. + */ +class PluginOptionGroup(pluginName: String) : OptionGroup(name = "Options for the $pluginName Plugin") { + val target: List by option( + "-t", + "--target", + help = "The files to be analyzed. May not always be source files depending on the plugin." + ) + .path(mustExist = true, mustBeReadable = true) + .multiple(required = true) + + val separate: Boolean by option( + "-s", + "--separate", + help = "Whether the plugin report should stay separate from the codyze report." + ) + .flag( + "--combined", + default = false, + defaultForHelp = "combined" + ) + + val output: File by option( + "-o", + "--output", + help = "The path of the resulting report. Only effective in combination with the \"--separate\" flag." + ) + .file() + .default(File("$pluginName.sarif")) + + val context: List by option( + "-c", + "--context", + help = "Additional context required for some plugins (e.g. auxiliary classpaths for FindSecBugs)." + ) + .path(mustExist = true, mustBeReadable = true) + .multiple(required = false) +} diff --git a/codyze-core/src/test/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/AggregateTest.kt b/codyze-core/src/test/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/AggregateTest.kt new file mode 100644 index 000000000..ea10de898 --- /dev/null +++ b/codyze-core/src/test/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/AggregateTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.core.output.aggregator + +import io.github.detekt.sarif4k.Notification +import io.github.detekt.sarif4k.Run +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeAll +import kotlin.test.Test +import kotlin.test.assertContains + +class AggregateTest { + + /** + * Tests the aggregation of a SARIF run containing extension tools + */ + @Test + fun aggregateExtensionsRun() { + val run = runs.first { it.first == "codyze-report.sarif" }.second + + Aggregate.addRun(run) + + val completeRun = Aggregate.createRun() + + assertEquals("CokoExecutor", completeRun!!.tool.driver.name) + assertEquals(1, completeRun.tool.extensions?.size ?: 0) + assertEquals(1, completeRun.results?.size ?: 0) + assertNotNull(completeRun.invocations) + assertTrue(completeRun.invocations!!.first().executionSuccessful) + assertEquals(listOf(), completeRun.invocations!!.first().toolExecutionNotifications) + } + + /** + * Tests the aggregation of a SARIF run containing an optional plugin + */ + @Test + fun aggregateMultipleRuns() { + val run1 = runs.first { it.first == "codyze-report.sarif" }.second + val run2 = runs.first { it.first == "pmd-report.sarif" }.second + + Aggregate.addRun(run1) + Aggregate.addRun(run2) + + val completeRun = Aggregate.createRun() + + assertEquals("CokoExecutor", completeRun!!.tool.driver.name) + assertEquals(2, completeRun.tool.extensions?.size ?: 0) + assertContains(completeRun.tool.extensions!!.map { it.name }, "CPG Coko Backend") + assertContains(completeRun.tool.extensions!!.map { it.name }, "PMD") + + assertEquals(4, completeRun.results?.size ?: 0) + assertEquals(4, completeRun.results?.mapNotNull { it.rule }?.size) + completeRun.results!! + .filterNot { it.rule!!.toolComponent!!.name == "CokoExecutor" } + .forEach { + assertTrue( + it.rule!!.toolComponent!!.name == "PMD" || + completeRun.tool.extensions!![it.rule!!.toolComponent!!.index?.toInt()!!].name == "PMD" + ) + } + } + + /** + * Tests the aggregation without a valid Codyze run + */ + @Test + fun aggregateNoDriver() { + val run = runs.first { it.first == "pmd-report.sarif" }.second + + Aggregate.addRun(run) + + val completeRun = Aggregate.createRun() + assertNull(completeRun) + } + + /** + * Tests the aggregation of a run that already uses rule objects in different formats + */ + @Test + fun aggregateRuleObjects() { + val run1 = runs.first { it.first == "codyze-report.sarif" }.second + val run2 = runs.first { it.first == "findsecbugs-report.sarif" }.second + + Aggregate.addRun(run1) + Aggregate.addRun(run2) + + val completeRun = Aggregate.createRun() + + assertEquals("CokoExecutor", completeRun!!.tool.driver.name) + assertFalse(completeRun.invocations?.get(0)?.executionSuccessful ?: true) + assertEquals(4, completeRun.tool.extensions?.size ?: 0) + assertEquals( + setOf("CPG Coko Backend", "SpotBugs", "edu.umd.cs.findbugs.plugins.core", "com.h3xstream.findsecbugs"), + completeRun.tool.extensions!!.map { it.name }.toSet() + ) + + assertEquals(4, completeRun.results?.size ?: 0) + assertEquals(4, completeRun.results?.mapNotNull { it.rule }?.size) + completeRun.results!! + .filterNot { it.rule!!.toolComponent!!.name == "CokoExecutor" } + .forEach { + assertTrue( + it.rule!!.toolComponent!!.name == "SpotBugs" || + completeRun.tool.extensions!![it.rule!!.toolComponent!!.index?.toInt()!!].name == "SpotBugs" + ) + } + } + + @AfterEach + fun resetAggregate() { + Aggregate.reset() + } + + companion object { + private lateinit var runs: Set> + + @BeforeAll + @JvmStatic + fun createRuns() { + ParserTest.loadResources() + runs = ParserTest.exampleResults.mapNotNull { + val run = extractLastRun(it) + if (run == null) null else it.name to run + }.toSet() + } + } +} diff --git a/codyze-core/src/test/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/ParserTest.kt b/codyze-core/src/test/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/ParserTest.kt new file mode 100644 index 000000000..d6143facd --- /dev/null +++ b/codyze-core/src/test/kotlin/de/fraunhofer/aisec/codyze/core/output/aggregator/ParserTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.core.output.aggregator + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import java.io.File +import java.net.URL +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.isRegularFile + +class ParserTest { + + /** + * Tests the parsing of a valid SARIF result file + */ + @Test + fun parseValid() { + val file = exampleResults.first { it.name == "pmd-report.sarif" } + + val run = extractLastRun(file) + + assertNotNull(run) + run!! + + assertEquals("PMD", run.tool.driver.name) + assertEquals(3, run.tool.driver.rules!!.size) + + assertEquals(3, run.results!!.size) + assertEquals("CloseResource", run.results!![0].ruleID) + assertEquals("CloseResource", run.results!![1].ruleID) + assertEquals("ControlStatementBraces", run.results!![2].ruleID) + } + + /** + * Tests the parsing of a file that does not exist + */ + @Test + fun parseNotExisting() { + val file = File("fake-results.sarif") + + val run = extractLastRun(file) + assertNull(run) + } + + /** + * Tests the parsing of a file that is not in valid SARIF format + */ + @Test + fun parseInvalidType() { + val file = exampleResults.first { it.name == "pmd-report.txt" } + + val run = extractLastRun(file) + assertNull(run) + } + + companion object { + lateinit var exampleResults: Set + + @BeforeAll + @JvmStatic + fun loadResources() { + val resultsDirectory: URL? = ParserTest::class.java.classLoader.getResource("externalReports") + + assertNotNull(resultsDirectory) + + exampleResults = Files.walk(Path.of(resultsDirectory!!.toURI())) + .filter { it.isRegularFile() } + .map { it.toFile() } + .toList().toSet() + } + } +} diff --git a/codyze-core/src/test/resources/externalReports/codyze-report.sarif b/codyze-core/src/test/resources/externalReports/codyze-report.sarif new file mode 100644 index 000000000..767c3cef2 --- /dev/null +++ b/codyze-core/src/test/resources/externalReports/codyze-report.sarif @@ -0,0 +1,249 @@ +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "artifacts": [ + { + "location": { + "uri": "/home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + } + } + ], + "results": [ + { + "kind": "fail", + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "index": 0, + "uri": "file:///home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + }, + "region": { + "endColumn": 91, + "endLine": 49, + "sourceLanguage": "JavaLanguage", + "startColumn": 8, + "startLine": 49 + } + } + } + ], + "message": { + "text": "Violation against rule in execution path from \"statement.executeUpdate(\"INSERT INTO data VALUES ('2006-01-05','very important')\");\". It is not followed by any of these calls: java.util.logging.Logger.info({.*, []})." + }, + "relatedLocations": [ + { + "physicalLocation": { + "artifactLocation": { + "index": 0, + "uri": "file:///home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + }, + "region": { + "endColumn": 91, + "endLine": 49, + "sourceLanguage": "JavaLanguage", + "startColumn": 8, + "startLine": 49 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "index": 0, + "uri": "file:///home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + }, + "region": { + "endColumn": 14, + "endLine": 50, + "sourceLanguage": "JavaLanguage", + "startColumn": 8, + "startLine": 50 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "index": 0, + "uri": "file:///home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + }, + "region": { + "endColumn": 14, + "endLine": 50, + "sourceLanguage": "JavaLanguage", + "startColumn": 8, + "startLine": 50 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "index": 0, + "uri": "file:///home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + }, + "region": { + "endColumn": 19, + "endLine": 50, + "sourceLanguage": "JavaLanguage", + "startColumn": 15, + "startLine": 50 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "index": 0, + "uri": "file:///home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + }, + "region": { + "endColumn": 30, + "endLine": 50, + "sourceLanguage": "JavaLanguage", + "startColumn": 20, + "startLine": 50 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "index": 0, + "uri": "file:///home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + }, + "region": { + "endColumn": 43, + "endLine": 50, + "sourceLanguage": "JavaLanguage", + "startColumn": 33, + "startLine": 50 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "index": 0, + "uri": "file:///home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + }, + "region": { + "endColumn": 43, + "endLine": 50, + "sourceLanguage": "JavaLanguage", + "startColumn": 20, + "startLine": 50 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "index": 0, + "uri": "file:///home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + }, + "region": { + "endColumn": 79, + "endLine": 50, + "sourceLanguage": "JavaLanguage", + "startColumn": 47, + "startLine": 50 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "index": 0, + "uri": "file:///home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + }, + "region": { + "endColumn": 79, + "endLine": 50, + "sourceLanguage": "JavaLanguage", + "startColumn": 20, + "startLine": 50 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "index": 0, + "uri": "file:///home/robert/AISEC/codyze/codyze-specification-languages/coko/coko-dsl/src/test/resources/java/Main.java" + }, + "region": { + "endColumn": 81, + "endLine": 50, + "sourceLanguage": "JavaLanguage", + "startColumn": 8, + "startLine": 50 + } + } + }, + { + "physicalLocation": { + "artifactLocation": { + "index": -1, + "uri": "null" + }, + "region": { + "sourceLanguage": "JavaLanguage" + } + } + } + ], + "ruleIndex": 0 + } + ], + "tool": { + "driver": { + "downloadUri": "https://github.com/Fraunhofer-AISEC/codyze/releases", + "informationUri": "https://www.codyze.io", + "name": "CokoExecutor", + "organization": "Fraunhofer AISEC", + "product": "Codyze", + "rules": [ + { + "defaultConfiguration": { + "level": "warning" + }, + "fullDescription": { + "text": "This is a dummy description." + }, + "help": { + "text": "" + }, + "id": "fun Model_codyze.DBActionsAreAlwaysLogged(Model_codyze.ObjectRelationalMapper, Model_codyze.Logging, Model_codyze.UserContext): de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.Evaluator", + "name": "DBActionsAreAlwaysLogged", + "properties": { + "tags": [ + ] + }, + "shortDescription": { + "text": "" + } + } + ], + "semanticVersion": "unspecified" + }, + "extensions": [ + { + "downloadUri": "https://github.com/Fraunhofer-AISEC/codyze/releases", + "informationUri": "https://www.codyze.io", + "isComprehensive": false, + "language": "en-US", + "name": "CPG Coko Backend", + "organization": "Fraunhofer AISEC", + "product": "Codyze", + "semanticVersion": "unspecified" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/codyze-core/src/test/resources/externalReports/findsecbugs-report.sarif b/codyze-core/src/test/resources/externalReports/findsecbugs-report.sarif new file mode 100644 index 000000000..1a638465c --- /dev/null +++ b/codyze-core/src/test/resources/externalReports/findsecbugs-report.sarif @@ -0,0 +1,253 @@ +{ + "version": "2.1.0", + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "extensions": [ + { + "version": "4.8.2", + "name": "edu.umd.cs.findbugs.plugins.core", + "shortDescription": { + "text": "Core SpotBugs plugin" + }, + "informationUri": "https://github.com/spotbugs", + "organization": "SpotBugs project" + }, + { + "version": "", + "name": "com.h3xstream.findsecbugs", + "shortDescription": { + "text": "Find Security Bugs" + }, + "informationUri": "https://find-sec-bugs.github.io", + "organization": "Find Security Bugs" + } + ], + "driver": { + "name": "SpotBugs", + "version": "4.8.2", + "language": "en", + "informationUri": "https://spotbugs.github.io/", + "rules": [ + { + "id": "DM_DEFAULT_ENCODING", + "shortDescription": { + "text": "Reliance on default encoding." + }, + "messageStrings": { + "default": { + "text": "Found reliance on default encoding in {0}: {1}." + } + }, + "helpUri": "https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html#DM_DEFAULT_ENCODING", + "properties": { + "tags": [ + "I18N" + ] + } + }, + { + "id": "PATH_TRAVERSAL_IN", + "shortDescription": { + "text": "Potential Path Traversal (file read)." + }, + "messageStrings": { + "default": { + "text": "This API ({0}) reads a file whose location might be specified by user input." + } + }, + "helpUri": "https://find-sec-bugs.github.io/bugs.htm#PATH_TRAVERSAL_IN", + "properties": { + "tags": [ + "SECURITY" + ] + }, + "relationships": [ + { + "target": { + "id": "22", + "guid": "19cf96fc-1234-5a3d-8e5d-21b225b7d3e8", + "toolComponent": { + "name": "CWE", + "guid": "b8c54a32-de19-51d2-9a08-f0abfbaa7310" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ], + "supportedTaxonomies": [ + { + "name": "CWE", + "guid": "b8c54a32-de19-51d2-9a08-f0abfbaa7310" + } + ] + } + }, + "invocations": [ + { + "exitCode": 3, + "exitSignalName": "ERROR,MISSING CLASS,BUGS FOUND", + "executionSuccessful": false, + "toolConfigurationNotifications": [ + { + "descriptor": { + "id": "spotbugs-missing-classes" + }, + "message": { + "text": "Classes needed for analysis were missing: [makeConcatWithConstants, accept]" + }, + "level": "error" + } + ] + } + ], + "results": [ + { + "rule": { + "id": "DM_DEFAULT_ENCODING", + "index": 0, + "toolComponent": { + "name": "SpotBugs" + } + }, + "message": { + "id": "default", + "text": "Reliance on default encoding", + "arguments": [ + "de.fraunhofer.aisec.codyze.medina.demo.jsse.TlsServer.start()", + "new java.io.InputStreamReader(InputStream)" + ] + }, + "level": "note", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "de/fraunhofer/aisec/codyze/medina/demo/jsse/TlsServer.java" + }, + "region": { + "startLine": 102 + } + }, + "logicalLocations": [ + { + "name": "new java.io.InputStreamReader(InputStream)", + "kind": "function", + "fullyQualifiedName": "new java.io.InputStreamReader(InputStream)" + } + ] + } + ] + }, + { + "rule": { + "id": "DM_DEFAULT_ENCODING", + "index": 0 + }, + "message": { + "id": "default", + "text": "Reliance on default encoding", + "arguments": [ + "de.fraunhofer.aisec.codyze.medina.demo.jsse.TlsServer.start()", + "new java.io.OutputStreamWriter(OutputStream)" + ] + }, + "level": "note", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "de/fraunhofer/aisec/codyze/medina/demo/jsse/TlsServer.java" + }, + "region": { + "startLine": 103 + } + }, + "logicalLocations": [ + { + "name": "new java.io.OutputStreamWriter(OutputStream)", + "kind": "function", + "fullyQualifiedName": "new java.io.OutputStreamWriter(OutputStream)" + } + ] + } + ] + }, + { + "rule": { + "id": "PATH_TRAVERSAL_IN", + "index": 1, + "toolComponent": { + "index": -1 + } + }, + "message": { + "id": "default", + "text": "Potential Path Traversal (file read)", + "arguments": [ + "java/io/File.\\u003cinit\\u003e(Ljava/lang/String;)V" + ] + }, + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "de/fraunhofer/aisec/codyze/medina/demo/jsse/TlsServer.java" + }, + "region": { + "startLine": 133 + } + }, + "logicalLocations": [ + { + "name": "main(String[])", + "kind": "function", + "fullyQualifiedName": "de.fraunhofer.aisec.codyze.medina.demo.jsse.TlsServer.main(String[])" + } + ] + } + ] + } + ], + "originalUriBaseIds": {}, + "taxonomies": [ + { + "name": "CWE", + "version": "4.10", + "minimumRequiredLocalizedDataSemanticVersion": "4.10", + "releaseDateUtc": "2023-01-31", + "guid": "b8c54a32-de19-51d2-9a08-f0abfbaa7310", + "informationUri": "https://cwe.mitre.org/data/published/cwe_v4.10.pdf/", + "downloadUri": "https://cwe.mitre.org/data/xml/cwec_v4.10.xml.zip", + "isComprehensive": true, + "organization": "MITRE", + "language": "en", + "shortDescription": { + "text": "The MITRE Common Weakness Enumeration" + }, + "taxa": [ + { + "id": "22", + "guid": "19cf96fc-1234-5a3d-8e5d-21b225b7d3e8", + "shortDescription": { + "text": "Improper Limitation of a Pathname to a Restricted Directory (\\u0027Path Traversal\\u0027)" + }, + "fullDescription": { + "text": "The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory." + }, + "defaultConfiguration": { + "level": "error" + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/codyze-core/src/test/resources/externalReports/pmd-report.sarif b/codyze-core/src/test/resources/externalReports/pmd-report.sarif new file mode 100644 index 000000000..f7f03dae7 --- /dev/null +++ b/codyze-core/src/test/resources/externalReports/pmd-report.sarif @@ -0,0 +1,152 @@ +{ + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "PMD", + "version": "7.0.0-rc4", + "informationUri": "https://docs.pmd-code.org/latest/", + "rules": [ + { + "id": "CloseResource", + "shortDescription": { + "text": "Ensure that resources like this BufferedReader object are closed after use" + }, + "fullDescription": { + "text": "\nEnsure that resources (like `java.sql.Connection`, `java.sql.Statement`, and `java.sql.ResultSet` objects\nand any subtype of `java.lang.AutoCloseable`) are always closed after use.\nFailing to do so might result in resource leaks.\n\nNote: It suffices to configure the super type, e.g. `java.lang.AutoCloseable`, so that this rule automatically triggers\non any subtype (e.g. `java.io.FileInputStream`). Additionally specifying `java.sql.Connection` helps in detecting\nthe types, if the type resolution / auxclasspath is not correctly setup.\n\nNote: Since PMD 6.16.0 the default value for the property `types` contains `java.lang.AutoCloseable` and detects\nnow cases where the standard `java.io.*Stream` classes are involved. In order to restore the old behaviour,\njust remove \"AutoCloseable\" from the types.\n " + }, + "helpUri": "https://docs.pmd-code.org/pmd-doc-7.0.0-rc4/pmd_rules_java_errorprone.html#closeresource", + "help": { + "text": "\nEnsure that resources (like `java.sql.Connection`, `java.sql.Statement`, and `java.sql.ResultSet` objects\nand any subtype of `java.lang.AutoCloseable`) are always closed after use.\nFailing to do so might result in resource leaks.\n\nNote: It suffices to configure the super type, e.g. `java.lang.AutoCloseable`, so that this rule automatically triggers\non any subtype (e.g. `java.io.FileInputStream`). Additionally specifying `java.sql.Connection` helps in detecting\nthe types, if the type resolution / auxclasspath is not correctly setup.\n\nNote: Since PMD 6.16.0 the default value for the property `types` contains `java.lang.AutoCloseable` and detects\nnow cases where the standard `java.io.*Stream` classes are involved. In order to restore the old behaviour,\njust remove \"AutoCloseable\" from the types.\n " + }, + "properties": { + "ruleset": "Error Prone", + "priority": 3, + "tags": [ + "Error Prone" + ] + } + }, + { + "id": "CloseResource", + "shortDescription": { + "text": "Ensure that resources like this BufferedWriter object are closed after use" + }, + "fullDescription": { + "text": "\nEnsure that resources (like `java.sql.Connection`, `java.sql.Statement`, and `java.sql.ResultSet` objects\nand any subtype of `java.lang.AutoCloseable`) are always closed after use.\nFailing to do so might result in resource leaks.\n\nNote: It suffices to configure the super type, e.g. `java.lang.AutoCloseable`, so that this rule automatically triggers\non any subtype (e.g. `java.io.FileInputStream`). Additionally specifying `java.sql.Connection` helps in detecting\nthe types, if the type resolution / auxclasspath is not correctly setup.\n\nNote: Since PMD 6.16.0 the default value for the property `types` contains `java.lang.AutoCloseable` and detects\nnow cases where the standard `java.io.*Stream` classes are involved. In order to restore the old behaviour,\njust remove \"AutoCloseable\" from the types.\n " + }, + "helpUri": "https://docs.pmd-code.org/pmd-doc-7.0.0-rc4/pmd_rules_java_errorprone.html#closeresource", + "help": { + "text": "\nEnsure that resources (like `java.sql.Connection`, `java.sql.Statement`, and `java.sql.ResultSet` objects\nand any subtype of `java.lang.AutoCloseable`) are always closed after use.\nFailing to do so might result in resource leaks.\n\nNote: It suffices to configure the super type, e.g. `java.lang.AutoCloseable`, so that this rule automatically triggers\non any subtype (e.g. `java.io.FileInputStream`). Additionally specifying `java.sql.Connection` helps in detecting\nthe types, if the type resolution / auxclasspath is not correctly setup.\n\nNote: Since PMD 6.16.0 the default value for the property `types` contains `java.lang.AutoCloseable` and detects\nnow cases where the standard `java.io.*Stream` classes are involved. In order to restore the old behaviour,\njust remove \"AutoCloseable\" from the types.\n " + }, + "properties": { + "ruleset": "Error Prone", + "priority": 3, + "tags": [ + "Error Prone" + ] + } + }, + { + "id": "ControlStatementBraces", + "shortDescription": { + "text": "This statement should have braces" + }, + "fullDescription": { + "text": "\n Enforce a policy for braces on control statements. It is recommended to use braces on 'if ... else'\n statements and loop statements, even if they are optional. This usually makes the code clearer, and\n helps prepare the future when you need to add another statement. That said, this rule lets you control\n which statements are required to have braces via properties.\n\n From 6.2.0 on, this rule supersedes WhileLoopMustUseBraces, ForLoopMustUseBraces, IfStmtMustUseBraces,\n and IfElseStmtMustUseBraces.\n " + }, + "helpUri": "https://docs.pmd-code.org/pmd-doc-7.0.0-rc4/pmd_rules_java_codestyle.html#controlstatementbraces", + "help": { + "text": "\n Enforce a policy for braces on control statements. It is recommended to use braces on 'if ... else'\n statements and loop statements, even if they are optional. This usually makes the code clearer, and\n helps prepare the future when you need to add another statement. That said, this rule lets you control\n which statements are required to have braces via properties.\n\n From 6.2.0 on, this rule supersedes WhileLoopMustUseBraces, ForLoopMustUseBraces, IfStmtMustUseBraces,\n and IfElseStmtMustUseBraces.\n " + }, + "properties": { + "ruleset": "Code Style", + "priority": 3, + "tags": [ + "Code Style" + ] + } + } + ] + } + }, + "results": [ + { + "ruleId": "CloseResource", + "ruleIndex": 0, + "message": { + "text": "Ensure that resources like this BufferedReader object are closed after use" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///home/robert/AISEC/tools/TlsServer.java" + }, + "region": { + "startLine": 102, + "startColumn": 32, + "endLine": 102, + "endColumn": 34 + } + } + } + ] + }, + { + "ruleId": "CloseResource", + "ruleIndex": 1, + "message": { + "text": "Ensure that resources like this BufferedWriter object are closed after use" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///home/robert/AISEC/tools/TlsServer.java" + }, + "region": { + "startLine": 103, + "startColumn": 32, + "endLine": 103, + "endColumn": 35 + } + } + } + ] + }, + { + "ruleId": "ControlStatementBraces", + "ruleIndex": 2, + "message": { + "text": "This statement should have braces" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///home/robert/AISEC/tools/TlsServer.java" + }, + "region": { + "startLine": 139, + "startColumn": 17, + "endLine": 139, + "endColumn": 37 + } + } + } + ] + } + ], + "invocations": [ + { + "executionSuccessful": true, + "toolConfigurationNotifications": [], + "toolExecutionNotifications": [] + } + ] + } + ] +} diff --git a/codyze-core/src/test/resources/externalReports/pmd-report.txt b/codyze-core/src/test/resources/externalReports/pmd-report.txt new file mode 100644 index 000000000..8e338441a --- /dev/null +++ b/codyze-core/src/test/resources/externalReports/pmd-report.txt @@ -0,0 +1,3 @@ +TlsServer.java:102: CloseResource: Ensure that resources like this BufferedReader object are closed after use +TlsServer.java:103: CloseResource: Ensure that resources like this BufferedWriter object are closed after use +TlsServer.java:139: ControlStatementBraces: This statement should have braces diff --git a/codyze-plugins/build.gradle.kts b/codyze-plugins/build.gradle.kts new file mode 100644 index 000000000..c9cc9b917 --- /dev/null +++ b/codyze-plugins/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("documented-module") + id("publish") +} + +repositories { + maven("https://dl.bintray.com/palantir/releases") +} + +dependencies { + implementation(libs.sarif4k) + implementation(libs.clikt) + implementation(libs.koin) + + implementation(projects.codyzeCore) + + /** + * When updating Plugins, make sure to update the documentation as well. + */ + // https://mvnrepository.com/artifact/com.github.spotbugs/spotbugs + // it is necessary to exclude saxon because of conflicts with same transitive dependency in PMD + implementation("com.github.spotbugs:spotbugs:4.8.2") { + exclude(group = "net.sf.saxon", module = "Saxon-HE") + } + implementation("com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0") + + // https://mvnrepository.com/artifact/net.sourceforge.pmd/ + implementation("net.sourceforge.pmd:pmd-core:7.0.0-rc4") + implementation("net.sourceforge.pmd:pmd-java:7.0.0-rc4") +} + +publishing { + publications { + named(name) { + pom { + name.set("Codyze External Results Aggregator") + description.set("Aggregator for results produced by external analysis tools") + } + } + } +} \ No newline at end of file diff --git a/codyze-plugins/src/main/kotlin/de/fraunhofer/aisec/codyze/plugins/FindSecBugsPlugin.kt b/codyze-plugins/src/main/kotlin/de/fraunhofer/aisec/codyze/plugins/FindSecBugsPlugin.kt new file mode 100644 index 000000000..ec6f6c66e --- /dev/null +++ b/codyze-plugins/src/main/kotlin/de/fraunhofer/aisec/codyze/plugins/FindSecBugsPlugin.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.plugins + +import de.fraunhofer.aisec.codyze.core.plugin.Plugin +import de.fraunhofer.aisec.codyze.core.plugin.logger +import edu.umd.cs.findbugs.BugReporter +import edu.umd.cs.findbugs.DetectorFactoryCollection +import edu.umd.cs.findbugs.FindBugs2 +import edu.umd.cs.findbugs.Plugin.loadCustomPlugin +import edu.umd.cs.findbugs.PluginException +import edu.umd.cs.findbugs.Project +import edu.umd.cs.findbugs.config.UserPreferences +import edu.umd.cs.findbugs.sarif.SarifBugReporter +import org.koin.core.module.Module +import org.koin.core.module.dsl.named +import org.koin.core.module.dsl.withOptions +import org.koin.dsl.bind +import java.io.File +import java.io.PrintWriter +import java.nio.file.Path + +class FindSecBugsPlugin : Plugin("FindSecBugs") { + // NOTE: this Executor will very likely mark the invocation as failed + // because of an (erroneous) missing class warning + // see: https://github.com/find-sec-bugs/find-sec-bugs/issues/692 + override fun execute(target: List, context: List, output: File) { + val project = Project() + + for (t in target) { + project.addFile(t.toString()) + } + + for (aux in context) { + project.addAuxClasspathEntry(aux.toString()) + } + + val reporter = SarifBugReporter(project) + if (output.parentFile != null) { + output.parentFile.mkdirs() + } + reporter.setWriter(PrintWriter(output.writer())) + reporter.setPriorityThreshold(BugReporter.NORMAL) + + // find and load Find Security Bugs plugin for SpotBugs + logger.debug { "Trying to locate 'Find Security Bugs' plugin for SpotBugs" } + val findSecBugsPlugin = javaClass.classLoader.getResources("findbugs.xml").toList().find { + it.toString().contains("findsecbugs-plugin") + } + + logger.info { "Found potential plugin location at $findSecBugsPlugin" } + findSecBugsPlugin?.run { + val pluginJar = Regex("^jar:file:(.*[.]jar)!/.*").replace(findSecBugsPlugin.toString(), "$1") + + logger.info { "Loading SpotBugs plugin 'Find Security Bugs' from JAR $pluginJar" } + try { + loadCustomPlugin(File(pluginJar), project) + } catch (e: PluginException) { + logger.warn { "Could not load FindSecBugs plugin from $pluginJar.\n$e" } + } + } ?: logger.warn { + "Could not load FindSecBugs plugin from $findSecBugsPlugin. Proceeding with default SpotBugs." + } + + val findbugs = FindBugs2() + findbugs.bugReporter = reporter + findbugs.project = project + findbugs.setDetectorFactoryCollection(DetectorFactoryCollection.instance()) + findbugs.userPreferences = UserPreferences.createDefaultUserPreferences() + findbugs.userPreferences.enableAllDetectors(true) + findbugs.execute() + } + + override fun module(): Module = org.koin.dsl.module { + factory { this@FindSecBugsPlugin } withOptions { + named("de.fraunhofer.aisec.codyze.plugins.FindSecBugsPlugin") + } bind (Plugin::class) + } +} diff --git a/codyze-plugins/src/main/kotlin/de/fraunhofer/aisec/codyze/plugins/PMDPlugin.kt b/codyze-plugins/src/main/kotlin/de/fraunhofer/aisec/codyze/plugins/PMDPlugin.kt new file mode 100644 index 000000000..bd53d8cc7 --- /dev/null +++ b/codyze-plugins/src/main/kotlin/de/fraunhofer/aisec/codyze/plugins/PMDPlugin.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.plugins + +import de.fraunhofer.aisec.codyze.core.plugin.Plugin +import net.sourceforge.pmd.PMDConfiguration +import net.sourceforge.pmd.PmdAnalysis +import org.koin.core.module.Module +import org.koin.core.module.dsl.named +import org.koin.core.module.dsl.withOptions +import org.koin.dsl.bind +import java.io.File +import java.nio.file.Path +import kotlin.io.path.pathString +import kotlin.io.path.writeBytes + +class PMDPlugin : Plugin("PMD") { + override fun execute(target: List, context: List, output: File) { + val config = PMDConfiguration() + for (path in target) { + config.addInputPath(path) + } + config.reportFormat = "sarif" + config.setReportFile(output.toPath()) + config.isIgnoreIncrementalAnalysis = true + + /** + * From https://github.com/pmd/pmd/tree/master/pmd-core/src/main/resources/ + * When adding more rule sets, remember to update the documentation. + * + * PMD does not handle complex paths well, so we use the following workaround + * TODO: let the user specify ruleset paths in the context + */ + val ruleset = javaClass.classLoader.getResourceAsStream("pmd-rulesets/all-java.xml")?.readAllBytes() + + println("\n\n\n $ruleset \n \n\n\n") + if (ruleset != null) { + val tempRuleSet = kotlin.io.path.createTempFile() + tempRuleSet.writeBytes(ruleset) + config.addRuleSet(tempRuleSet.pathString) + } + + val analysis = PmdAnalysis.create(config) + analysis.performAnalysis() + } + + override fun module(): Module = org.koin.dsl.module { + factory { this@PMDPlugin } withOptions { + named("de.fraunhofer.aisec.codyze.plugins.PMDPlugin") + } bind (Plugin::class) + } +} diff --git a/codyze-plugins/src/main/resources/META-INF/services/de.fraunhofer.aisec.codyze.core.plugin.Plugin b/codyze-plugins/src/main/resources/META-INF/services/de.fraunhofer.aisec.codyze.core.plugin.Plugin new file mode 100644 index 000000000..45ad41f53 --- /dev/null +++ b/codyze-plugins/src/main/resources/META-INF/services/de.fraunhofer.aisec.codyze.core.plugin.Plugin @@ -0,0 +1,2 @@ +de.fraunhofer.aisec.codyze.plugins.FindSecBugsPlugin +de.fraunhofer.aisec.codyze.plugins.PMDPlugin \ No newline at end of file diff --git a/codyze-plugins/src/main/resources/pmd-rulesets/all-java.xml b/codyze-plugins/src/main/resources/pmd-rulesets/all-java.xml new file mode 100644 index 000000000..8764654cf --- /dev/null +++ b/codyze-plugins/src/main/resources/pmd-rulesets/all-java.xml @@ -0,0 +1,38 @@ + + + + Every Java Rule in PMD + + + + .*/ant/java/EncodingTestClass.java + .*/net/sourceforge/pmd/cpd/badandgood/BadFile.java + + + .*/net/sourceforge/pmd/lang/java/ast/jdkversiontests/assert_test5.java + .*/net/sourceforge/pmd/lang/java/ast/jdkversiontests/assert_test5_a.java + .*/net/sourceforge/pmd/lang/java/ast/jdkversiontests/assert_test7.java + .*/net/sourceforge/pmd/lang/java/ast/jdkversiontests/jdk14_enum.java + .*/net/sourceforge/pmd/lang/java/ast/jdkversiontests/jdk9_invalid_identifier.java + .*/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java10/LocalVariableTypeInference_varAsAnnotationName.java + .*/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java10/LocalVariableTypeInference_varAsEnumName.java + .*/net/sourceforge/pmd/lang/java/ast/jdkversiontests/java10/LocalVariableTypeInference_varAsTypeIdentifier.java + + + .*/net/sourceforge/pmd/lang/java/ast/InfiniteLoopInLookahead.java + + + + + + + + + + + diff --git a/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/PluginTest.kt b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/PluginTest.kt new file mode 100644 index 000000000..3270bc2eb --- /dev/null +++ b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/PluginTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.plugins + +import de.fraunhofer.aisec.codyze.core.output.aggregator.extractLastRun +import de.fraunhofer.aisec.codyze.core.plugin.Plugin +import io.github.detekt.sarif4k.Result +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import java.io.File +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +abstract class PluginTest { + abstract val plugin: Plugin + abstract val resultFileName: String + open val expectedSuccess: Boolean = true + abstract val expectedResults: List + + @Test + fun testResults() { + scanFiles() + val resultURI = PluginTest::class.java.classLoader.getResource("generatedReports/$resultFileName")?.toURI() + assertNotNull(resultURI) + val run = extractLastRun(File(resultURI)) + assertNotNull(run) + + var results = run.results + assertNotNull(results) + assertEquals(expectedResults.size, results.size) + // do not test the physical artifact location as it differs per system + results = results.map { + it.copy( + locations = it.locations?.map + { + location -> + location.copy(physicalLocation = location.physicalLocation?.copy(artifactLocation = null)) + } + ) + } + assertContentEquals(expectedResults, results) + } + + @Test + fun testInvocation() { + scanFiles() + val resultURI = PluginTest::class.java.classLoader.getResource("generatedReports/$resultFileName")?.toURI() + assertNotNull(resultURI) + val run = extractLastRun(File(resultURI)) + assertNotNull(run) + + if (!run.invocations.isNullOrEmpty()) { + // We do not always expect success because tools like FindSecBugs report no success. + // This is because of a known bug with lambdas being reported as missing references + run.invocations!!.forEach { assertEquals(expectedSuccess, it.executionSuccessful) } + } + } + + @AfterEach + fun cleanup() { + val resultURI = PluginTest::class.java.classLoader.getResource("generatedReports/$resultFileName")?.toURI() + if (resultURI != null) { + File(resultURI).delete() + } + } + + /** + * Executes the respective executor with the correct Paths + */ + abstract fun scanFiles() +} diff --git a/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/compiled/CompiledPluginTest.kt b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/compiled/CompiledPluginTest.kt new file mode 100644 index 000000000..80c92c34e --- /dev/null +++ b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/compiled/CompiledPluginTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.plugins.compiled + +import de.fraunhofer.aisec.codyze.plugins.PluginTest +import java.nio.file.Path +import kotlin.test.assertNotNull + +abstract class CompiledPluginTest : PluginTest() { + override fun scanFiles() { + val libPath = PluginTest::class.java.classLoader.getResource("targets/libs/demo-cloud-service-1.0.0.jar")?.path + val contextPaths = listOf( + PluginTest::class.java.classLoader.getResource("targets/libs/bcpkix-jdk18on-1.75.jar")?.path, + PluginTest::class.java.classLoader.getResource("targets/libs/bcprov-jdk18on-1.75.jar")?.path, + PluginTest::class.java.classLoader.getResource("targets/libs/bctls-jdk18on-1.75.jar")?.path, + PluginTest::class.java.classLoader.getResource("targets/libs/bcutil-jdk18on-1.75.jar")?.path + ) + assertNotNull(libPath) + + plugin.execute( + listOf(Path.of(libPath)), + contextPaths.map { Path.of(it!!) }, + Path.of(libPath).parent.parent.parent.resolve("generatedReports").resolve(resultFileName).toFile() + ) + } +} diff --git a/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/compiled/FindSecBugsPluginTest.kt b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/compiled/FindSecBugsPluginTest.kt new file mode 100644 index 000000000..6d1578ed1 --- /dev/null +++ b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/compiled/FindSecBugsPluginTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.plugins.compiled + +import de.fraunhofer.aisec.codyze.plugins.FindSecBugsPlugin +import io.github.detekt.sarif4k.* + +class FindSecBugsPluginTest : CompiledPluginTest() { + override val plugin = FindSecBugsPlugin() + override val resultFileName = "findsecbugs.sarif" + override val expectedSuccess = false + override val expectedResults = listOf( + Result( + ruleID = "DM_DEFAULT_ENCODING", + ruleIndex = 0, + message = Message( + id = "default", + text = "Reliance on default encoding", + arguments = listOf( + "de.fraunhofer.aisec.codyze.medina.demo.jsse.TlsServer.start()", + "new java.io.InputStreamReader(InputStream)" + ) + ), + level = Level.Note, + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region(startLine = 102) + ), + logicalLocations = listOf( + LogicalLocation( + name = "new java.io.InputStreamReader(InputStream)", + kind = "function", + fullyQualifiedName = "new java.io.InputStreamReader(InputStream)" + ) + ) + ) + ) + ), + Result( + ruleID = "DM_DEFAULT_ENCODING", + ruleIndex = 0, + message = Message( + id = "default", + text = "Reliance on default encoding", + arguments = listOf( + "de.fraunhofer.aisec.codyze.medina.demo.jsse.TlsServer.start()", + "new java.io.OutputStreamWriter(OutputStream)" + ) + ), + level = Level.Note, + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region(startLine = 103) + ), + logicalLocations = listOf( + LogicalLocation( + name = "new java.io.OutputStreamWriter(OutputStream)", + kind = "function", + fullyQualifiedName = "new java.io.OutputStreamWriter(OutputStream)" + ) + ) + ) + ) + ), + Result( + ruleID = "PATH_TRAVERSAL_IN", + ruleIndex = 1, + message = Message( + id = "default", + text = "Potential Path Traversal (file read)", + arguments = listOf("java/io/File.(Ljava/lang/String;)V") + ), + level = Level.Warning, + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region(startLine = 133) + ), + logicalLocations = listOf( + LogicalLocation( + name = "main(String[])", + kind = "function", + fullyQualifiedName = "de.fraunhofer.aisec.codyze.medina.demo.jsse.TlsServer.main(String[])" + ) + ) + ) + ) + ) + ) +} diff --git a/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/source/PMDPluginTest.kt b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/source/PMDPluginTest.kt new file mode 100644 index 000000000..15a0a66dc --- /dev/null +++ b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/source/PMDPluginTest.kt @@ -0,0 +1,501 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.plugins.source + +import de.fraunhofer.aisec.codyze.plugins.PMDPlugin +import io.github.detekt.sarif4k.* + +class PMDPluginTest : SourcePluginTest() { + override val plugin = PMDPlugin() + override val resultFileName = "pmd.sarif" + override val expectedResults = listOf( + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 24, + startColumn = 13, + endLine = 24, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 27, + startColumn = 17, + endLine = 27, + endColumn = 23 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 29, + startColumn = 13, + endLine = 29, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 44, + startColumn = 13, + endLine = 44, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 45, + startColumn = 13, + endLine = 45, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 46, + startColumn = 13, + endLine = 46, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 48, + startColumn = 13, + endLine = 48, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 49, + startColumn = 13, + endLine = 49, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 50, + startColumn = 13, + endLine = 50, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 54, + startColumn = 13, + endLine = 54, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 56, + startColumn = 13, + endLine = 56, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 58, + startColumn = 13, + endLine = 58, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 59, + startColumn = 13, + endLine = 59, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 60, + startColumn = 13, + endLine = 60, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 62, + startColumn = 13, + endLine = 62, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 64, + startColumn = 13, + endLine = 64, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 87, + startColumn = 13, + endLine = 87, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 89, + startColumn = 13, + endLine = 89, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 91, + startColumn = 13, + endLine = 91, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 93, + startColumn = 13, + endLine = 93, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 95, + startColumn = 13, + endLine = 95, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "AvoidPrintStackTrace", + ruleIndex = 1, + message = Message( + text = "Avoid printStackTrace(); use a logger call instead.", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 109, + startColumn = 17, + endLine = 109, + endColumn = 36 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 120, + startColumn = 13, + endLine = 120, + endColumn = 19 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 124, + startColumn = 17, + endLine = 124, + endColumn = 23 + ) + ) + ) + ) + ), + Result( + ruleID = "SystemPrintln", + ruleIndex = 0, + message = Message( + text = "Usage of System.out/err", + ), + locations = listOf( + Location( + physicalLocation = PhysicalLocation( + region = Region( + startLine = 126, + startColumn = 13, + endLine = 126, + endColumn = 19 + ) + ) + ) + ) + ), + ) +} diff --git a/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/source/SourcePluginTest.kt b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/source/SourcePluginTest.kt new file mode 100644 index 000000000..1e0d7a479 --- /dev/null +++ b/codyze-plugins/src/test/kotlin/de/fraunhofer/aisec/codyze/plugins/source/SourcePluginTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://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. + */ +package de.fraunhofer.aisec.codyze.plugins.source + +import de.fraunhofer.aisec.codyze.plugins.PluginTest +import java.nio.file.Path +import kotlin.test.assertNotNull + +abstract class SourcePluginTest : PluginTest() { + override fun scanFiles() { + val sourcePath = PluginTest::class.java.classLoader.getResource("targets/TlsServer.java")?.path + assertNotNull(sourcePath) + + plugin.execute( + listOf(Path.of(sourcePath)), + listOf(), + Path.of(sourcePath).parent.parent.resolve("generatedReports").resolve(resultFileName).toFile() + ) + } +} diff --git a/codyze-plugins/src/test/resources/targets/TlsServer.java b/codyze-plugins/src/test/resources/targets/TlsServer.java new file mode 100644 index 000000000..e90bbb00e --- /dev/null +++ b/codyze-plugins/src/test/resources/targets/TlsServer.java @@ -0,0 +1,160 @@ +package de.fraunhofer.aisec.codyze.medina.demo.jsse; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; + +import javax.net.ssl.*; +import java.io.*; +import java.net.Socket; +import java.security.*; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.Optional; + +public class TlsServer { + + private static final boolean DEBUG = true; + private static final int PORT = 8443; + + private SSLServerSocket socket; + + private void configure(int port, String keystore, String keystorePwd) throws IOException, NoSuchAlgorithmException, KeyStoreException, CertificateException, UnrecoverableKeyException, KeyManagementException, NoSuchProviderException { + // overview of functionality provided by BCJSSE + if (DEBUG) { + System.out.println("Services provided by BCJSSE security Provider"); + + for (Provider.Service s : Security.getProvider("BCJSSE").getServices()) { + System.out.println(s); + } + System.out.println(); + } + + // get default from most prioritized securtiy provider -> BCJSSE + SSLContext sslCtx = SSLContext.getInstance("TLS", "BCJSSE"); + + // initialize sslContext with a KeyManager and no TrustManager + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(new FileInputStream(keystore), keystorePwd.toCharArray()); + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, keystorePwd.toCharArray()); + sslCtx.init(kmf.getKeyManagers(), null, null); + + // verify correct selection + if(DEBUG) { + System.out.println("Current provider:"); + System.out.println(sslCtx.getProvider()); + System.out.println(); + + System.out.println("Selected protocol:"); + System.out.println(sslCtx.getProtocol()); + System.out.println(); + + SSLParameters sslParams = sslCtx.getSupportedSSLParameters(); + + System.out.println("Protocols:"); + Arrays.stream(sslParams.getProtocols()).forEach(System.out::println); + System.out.println(); + + System.out.println("Use cipher suites order: "); + System.out.println(sslParams.getUseCipherSuitesOrder()); + System.out.println(); + + System.out.println("Cipher suites:"); + Arrays.stream(sslParams.getCipherSuites()).forEach(System.out::println); + System.out.println(); + } + + // create the SSLServerSocket + SSLServerSocketFactory socketFactory = sslCtx.getServerSocketFactory(); + socket = (SSLServerSocket) socketFactory.createServerSocket(port); + + // set protocol versions and cipher suites + socket.setEnabledProtocols(new String[]{ + "TLSv1.1", // FORBIDDEN + "TLSv1.2" + }); + socket.setEnabledCipherSuites(new String[]{ + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_AES_128_CCM_SHA256", // FORBIDDEN + "TLS_AES_128_GCM_SHA256", // FORBIDDEN + "TLS_AES_256_GCM_SHA384" // FORBIDDEN + }); + + if (DEBUG) { + System.out.println("Enabled by the Socket:"); + + System.out.println("Protocols:"); + Arrays.stream(socket.getEnabledProtocols()).forEach(System.out::println); + System.out.println(); + + System.out.println("Cipher suites:"); + Arrays.stream(socket.getEnabledCipherSuites()).forEach(System.out::println); + System.out.println(); + } + } + + private void start() { + while(true) { + try (Socket sock = socket.accept()) { + BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream())); + BufferedWriter out = new BufferedWriter(new OutputStreamWriter(sock.getOutputStream())); + while (in.readLine() != null) { + out.write("ack"); + out.flush(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public static void main(String[] args) { + // ensure Bouncy Castle is first provider queried when retrieving implementations + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + Security.insertProviderAt(new BouncyCastleProvider(), 2); + + if (DEBUG) { + System.out.println("Registered security providers:"); + + Provider[] ps = Security.getProviders(); + for (int i = 0; i < ps.length; i++) { + System.out.println(i + " : " + ps[i]); + } + System.out.println(); + } + + // try to get the path of the needed Keystore: + File jks = null; + // 1: absolute path via program argument + if (args.length != 0) { + jks = new File(args[0]); + } + // 2: absolute path via environment variable + if (args.length == 0 || !jks.exists()) { + String env = System.getenv("KEYSTORE_PATH"); + if (env != null) + jks = new File(env); + //3: load via class loader + if (env == null || !jks.exists()) { + // Throws a NPE if Keystore could not be found up until this point + jks = new File(TlsServer.class.getClassLoader().getResource("keystore.jks").getPath()); + } + } + + String keyStorePwd = Optional.ofNullable(System.getenv("KEYSTORE_PWD")).orElse("demo-password"); + + // create TLS server + TlsServer server = new TlsServer(); + try { + // the keystore is expected to be generated with the script in the resource folder + server.configure(PORT, jks.getAbsolutePath(), keyStorePwd); + } catch (Exception e) { + e.getLocalizedMessage(); + System.exit(1); + } + server.start(); + } +} diff --git a/codyze-plugins/src/test/resources/targets/libs/bcpkix-jdk18on-1.75.jar b/codyze-plugins/src/test/resources/targets/libs/bcpkix-jdk18on-1.75.jar new file mode 100644 index 000000000..24f2f7032 Binary files /dev/null and b/codyze-plugins/src/test/resources/targets/libs/bcpkix-jdk18on-1.75.jar differ diff --git a/codyze-plugins/src/test/resources/targets/libs/bcprov-jdk18on-1.75.jar b/codyze-plugins/src/test/resources/targets/libs/bcprov-jdk18on-1.75.jar new file mode 100644 index 000000000..e88367c76 Binary files /dev/null and b/codyze-plugins/src/test/resources/targets/libs/bcprov-jdk18on-1.75.jar differ diff --git a/codyze-plugins/src/test/resources/targets/libs/bctls-jdk18on-1.75.jar b/codyze-plugins/src/test/resources/targets/libs/bctls-jdk18on-1.75.jar new file mode 100644 index 000000000..891faa832 Binary files /dev/null and b/codyze-plugins/src/test/resources/targets/libs/bctls-jdk18on-1.75.jar differ diff --git a/codyze-plugins/src/test/resources/targets/libs/bcutil-jdk18on-1.75.jar b/codyze-plugins/src/test/resources/targets/libs/bcutil-jdk18on-1.75.jar new file mode 100644 index 000000000..48111b813 Binary files /dev/null and b/codyze-plugins/src/test/resources/targets/libs/bcutil-jdk18on-1.75.jar differ diff --git a/codyze-plugins/src/test/resources/targets/libs/demo-cloud-service-1.0.0.jar b/codyze-plugins/src/test/resources/targets/libs/demo-cloud-service-1.0.0.jar new file mode 100644 index 000000000..67460ce09 Binary files /dev/null and b/codyze-plugins/src/test/resources/targets/libs/demo-cloud-service-1.0.0.jar differ diff --git a/codyze-specification-languages/coko/coko-dsl/src/test/kotlin/de/fraunhofer/aisec/codyze/specificationLanguages/coko/dsl/cli/CokoOptionGroupTest.kt b/codyze-specification-languages/coko/coko-dsl/src/test/kotlin/de/fraunhofer/aisec/codyze/specificationLanguages/coko/dsl/cli/CokoOptionGroupTest.kt index 769533cce..75c32e9f7 100644 --- a/codyze-specification-languages/coko/coko-dsl/src/test/kotlin/de/fraunhofer/aisec/codyze/specificationLanguages/coko/dsl/cli/CokoOptionGroupTest.kt +++ b/codyze-specification-languages/coko/coko-dsl/src/test/kotlin/de/fraunhofer/aisec/codyze/specificationLanguages/coko/dsl/cli/CokoOptionGroupTest.kt @@ -85,7 +85,9 @@ class CokoOptionGroupTest : KoinTest { val exception: Exception = Assertions.assertThrows(IllegalArgumentException::class.java) { cli.executorOptions.spec } - val expectedMessage = "All given specification files must be coko specification files (*.codyze.kts)." + val expectedMessage = + "All given specification files must be coko specification files (*.codyze.kts) or concept files " + + "(*.concepts)." val actualMessage = exception.message.orEmpty() assertContains(actualMessage, expectedMessage) diff --git a/docs/Getting Started/configuration.md b/docs/Getting Started/configuration.md index aa3bde269..c14656f2d 100644 --- a/docs/Getting Started/configuration.md +++ b/docs/Getting Started/configuration.md @@ -56,7 +56,7 @@ The names are the same for the configuration file and the CLI options. |:--------------|:--------|:-----------------------------------------------------------------------------------------------------------------------|:--------------| | output | Path | The path to write the results file to. | `[./]` | | output-format | String | Format in which the analysis results are returned. | `sarif` | -| good-findings | Boolean | Enable/Disable output of "positive" findings which indicate correct implementations. | `true` | +| good-findings | Boolean | Enable/Disable output of "positive" findings which indicate correct implementations. | `true` | | pedantic | Boolean | Activates pedantic analysis mode. In this mode, Codyze analyzes all given specification files and report all findings. | `false` | ### Executors diff --git a/docs/Plugins/FindSecBugs.md b/docs/Plugins/FindSecBugs.md new file mode 100644 index 000000000..a5e449f9d --- /dev/null +++ b/docs/Plugins/FindSecBugs.md @@ -0,0 +1,31 @@ +--- +title: "FindSecBugs Plugin" +linkTitle: "FindSecBugs Plugin" +no_list: true +date: 2023-01-24 +description: > + The FindSecBugs Plugin aims to report security bugs in compiled Java code. +--- + +!!! info + + Check out the official site [here](https://find-sec-bugs.github.io/). + +## Plugin overview + +FindSecBugs is an extension of the SpotBugs analyzer that works on compiled Java code. +It focuses on finding security-critical bugs such as potential code injections. + +!!! bug + + Using the FindSecBugs plugin may mark the analysis run as unsuccessful when using lambdas. + This is a [known issue](https://github.com/find-sec-bugs/find-sec-bugs/issues/692) within SpotBugs + +!!! question "How does FindSecBugs use the context?" + + FindSecBugs relies on the compiled code of the libraries to resolve all code references. + Therefore, the context should point to those libraries in order to ensure a complete analysis. + + + + diff --git a/docs/Plugins/PMD.md b/docs/Plugins/PMD.md new file mode 100644 index 000000000..503485add --- /dev/null +++ b/docs/Plugins/PMD.md @@ -0,0 +1,29 @@ +--- +title: "PMD Plugin" +linkTitle: "PMD Plugin" +no_list: true +date: 2023-01-24 +description: > + The PMD Plugin aims to report security bugs in compiled Java code. +--- + +!!! info + + Check out the official site [here](https://pmd.github.io/). + +## Plugin overview + +PMD is a source code analyzer that searches for common programming flaws. +It supports many different languages and can be extended by different sets of rules. + +In its current implementation the plugin uses the following sets of rules: + - all-java.xml ([link](https://github.com/pmd/pmd/blob/83522e96ef512f2b9a41586ae239509ec6f8313f/pmd-core/src/main/resources/rulesets/internal/all-java.xml)) + +!!! note + + These rules define the supported languages as well as the flaws found in those languages. + They may be extended in future updates. + +!!! question "How does PMD use the context?" + + PMD does not rely on addition context, this option is therefore ignored. \ No newline at end of file diff --git a/docs/Plugins/index.md b/docs/Plugins/index.md new file mode 100644 index 000000000..de1eee4b6 --- /dev/null +++ b/docs/Plugins/index.md @@ -0,0 +1,51 @@ +--- +title: "Plugin Overview" +linkTitle: "Plugin Overview" +no_list: true +date: 2023-01-24 +description: > + Plugins offer a way to incorporate external tools into your analysis. +--- + +!!! example "Experimental" + + This feature is disabled by default and has to be manually enabled in the gradle.properties file before building. + + +There are many code analysis tools available, and they frequently yield a wide range of different results. +However, finding and configuring the correct tools for your projects is time-consuming and distributes the results into separate report files. +Plugins are designed to make adding new tools easier and to allow for quickly swapping preferred analysis methods. + +## What are Plugins? + +Plugins are - as the name suggests - modular analysis additions that run other open-source tools. +Since those tools are independent of each other, the same finding may occur multiple times if they are reported by more than one tool. + +By default, Codyze creates a single consolidated report for all analysis runs by combining the reports generated by the plugins into its primary output file. +This behaviour can be toggled in the configuration of each plugin. + +## Configuration Options + +Each plugin can be configured through the following options: + +| Key | Value | Description | Mandatory | +|:------------------|:--------|:------------------------------------------------|:---------:| +| target | Path[] | The target file to be analyzed by the plugin | Yes | +| context | Path[] | Additional plugin-dependent context | (Yes/No) | +| separate/combined | Boolean | Whether the plugin report should be standalone | No | +| output | File | The location of the plugin report. | No | + +The `context` adds a way of giving additional information to a plugin that may be necessary to complete the analysis. Therefore, the specific plugin defines whether this option is necessary, optional or ignored. + +The default result format is a `combined` report file. In this case, the `output` option is ignored + +## Available Plugins + +!!! note + + The list of available plugins may expand in future updates. + +| Name | Version | Source | Website | Analysis Target | +|:----------------------------|:-----------------:|:------------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------:|:-------------------| +| PMD | 7.0.0-rc4 | [GitHub](https://github.com/pmd/pmd) | [github.io](https://pmd.github.io/) | Source Code | +| FindSecBugs
(SpotBugs) | 1.12.0
4.8.2 | [GitHub](https://github.com/find-sec-bugs/find-sec-bugs)
[GitHub](https://github.com/spotbugs/spotbugs) | [github.io](https://find-sec-bugs.github.io/)
[github.io](https://spotbugs.github.io/) | Compiled Java Code | \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index cb8ddd9cb..305e751c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,3 +6,6 @@ kotlin.code.style=official # Gradle HTTP timeout for `-SNAPSHOT` from JitPack -> 180000 ms == 3 min systemProp.org.gradle.internal.http.connectionTimeout=180000 systemProp.org.gradle.internal.http.socketTimeout=180000 + +# Experimental features +enablePluginSupport=false diff --git a/settings.gradle.kts b/settings.gradle.kts index da116a50b..799329d8e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,12 @@ include(":codyze-backends:cpg") include(":codyze-specification-languages:coko:coko-core") include(":codyze-specification-languages:coko:coko-dsl") -// TODO re-enable modules once adapted to codyze v3 -// include("codyze-lsp") -// include("codyze-console") +/* + * Optional and experimental features + */ +// Support external plugins, e.g. code analysis tools +val enablePluginSupport: Boolean by extra { + val enablePluginSupport: String? by settings + enablePluginSupport.toBoolean() +} +if (enablePluginSupport) include(":codyze-plugins")