diff --git a/buildSrc/src/main/kotlin/features.gradle.kts b/buildSrc/src/main/kotlin/features.gradle.kts index e9ccfbed8..e27914ba9 100644 --- a/buildSrc/src/main/kotlin/features.gradle.kts +++ b/buildSrc/src/main/kotlin/features.gradle.kts @@ -6,4 +6,4 @@ val enablePluginSupport: Boolean by rootProject.extra dependencies { if (enablePluginSupport) runtimeOnly(project(":codyze-plugins")) -} \ No newline at end of file +} diff --git a/codyze-cli/build.gradle.kts b/codyze-cli/build.gradle.kts index 2a401f363..998a6fcb7 100644 --- a/codyze-cli/build.gradle.kts +++ b/codyze-cli/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { implementation(projects.codyzeCore) implementation(projects.codyzeBackends.cpg) implementation(projects.codyzeSpecificationLanguages.coko.cokoDsl) + implementation(projects.codyzeSpecificationLanguages.cpgNative) implementation(libs.clikt) implementation(libs.koin) 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 12386ccad..eb24ddd85 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 @@ -24,6 +24,7 @@ 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.specificationLanguage.cpg.native.CPGQuerySubcommand import de.fraunhofer.aisec.codyze.specificationLanguages.coko.dsl.cli.CokoSubcommand import org.koin.core.module.dsl.factoryOf import org.koin.dsl.bind @@ -42,6 +43,7 @@ val backendCommands = module { * Each [Executor] must provide a [ExecutorCommand] to be selectable in the CLI. */ val executorCommands = module { + factoryOf(::CPGQuerySubcommand) bind(ExecutorCommand::class) factoryOf(::CokoSubcommand) bind(ExecutorCommand::class) } diff --git a/codyze-specification-languages/cpg-native/build.gradle.kts b/codyze-specification-languages/cpg-native/build.gradle.kts new file mode 100644 index 000000000..e2f6b121f --- /dev/null +++ b/codyze-specification-languages/cpg-native/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("documented-module") + id("publish") +} + +dependencies { + implementation(projects.codyzeCore) + implementation(projects.codyzeSpecificationLanguages.coko.cokoCore) + implementation(projects.codyzeBackends.cpg) // used only for the CokoScript plugin block configuration + implementation(libs.bundles.cpg) + implementation(libs.kotlin.reflect) + + implementation(libs.sarif4k) + implementation(libs.koin) + implementation(libs.clikt) + + // For testing with koin + // kotlin-test-junit has to be excluded because it is loaded by "documented-module" plugin + testImplementation(libs.koin.test) { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-test-junit") + } + testImplementation(libs.koin.junit5) { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-test-junit") + } + testImplementation(libs.mockk) + testImplementation(libs.bundles.cpg) +} + +publishing { + publications { + named(name) { + pom { + name.set("Codyze Specification Language Native CPG Query DSL") + description.set("Queries with native CPG DSL for Codyze") + } + } + } +} diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryConfiguration.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryConfiguration.kt new file mode 100644 index 000000000..6e872483c --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryConfiguration.kt @@ -0,0 +1,25 @@ +/* + * 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.specificationLanguage.cpg.native + +import de.fraunhofer.aisec.codyze.core.executor.ExecutorConfiguration +import io.github.oshai.kotlinlogging.KotlinLogging + +private val logger = KotlinLogging.logger { } + +data class CPGQueryConfiguration( + val runQueries: Boolean // Queries may be turned of, if all executors are run and queries shoul be excluded +) : ExecutorConfiguration diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryExecutor.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryExecutor.kt new file mode 100644 index 000000000..d1a6a1c9e --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryExecutor.kt @@ -0,0 +1,60 @@ +/* + * 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.specificationLanguage.cpg.native + +import de.fraunhofer.aisec.codyze.backends.cpg.CPGBackend +import de.fraunhofer.aisec.codyze.core.executor.Executor +import de.fraunhofer.aisec.codyze.specificationLanguage.cpg.native.queries.CPGQuery +import de.fraunhofer.aisec.codyze.specificationLanguage.cpg.native.queries.ExampleQuery +import io.github.detekt.sarif4k.Run +import io.github.oshai.kotlinlogging.KotlinLogging +import java.io.FileOutputStream +import java.io.PrintStream + +private val logger = KotlinLogging.logger {} + +/** + * The [Executor] to run natively defined CPG queries on the cpg backend, generating Sarif output + */ + +class CPGQueryExecutor(private val configuration: CPGQueryConfiguration, private val backend: CPGBackend) : + Executor { + private val queries: MutableList = mutableListOf() + + init { + queries.add(ExampleQuery()) + } + + override fun evaluate(): Run { + logger.info { "Running CPG Queries" } + val findings: MutableMap> = mutableMapOf() + + queries.forEach { + findings.put(it, it.query(backend)) + } + val informationExtractor = TSFIInformationExtractor() + informationExtractor.extractInformation(backend.cpg) + + informationExtractor.printInformation( + XMLFormatter(), + PrintStream(FileOutputStream("sf.xml")), + PrintStream(FileOutputStream("tsfi.xml")) + ) + + val cpgQuerySarifBuilder = CPGQuerySarifBuilder(queries = queries, backend = backend) + return cpgQuerySarifBuilder.buildRun(findings = findings) + } +} diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryFinding.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryFinding.kt new file mode 100644 index 000000000..6b30ac3a8 --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryFinding.kt @@ -0,0 +1,52 @@ +/* + * 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.specificationLanguage.cpg.native + +import de.fraunhofer.aisec.codyze.backends.cpg.coko.getSarifLocation +import de.fraunhofer.aisec.codyze.specificationLanguage.cpg.native.queries.CPGQuery +import de.fraunhofer.aisec.codyze.specificationLanguages.coko.core.Finding +import de.fraunhofer.aisec.cpg.graph.Node +import io.github.detekt.sarif4k.Artifact +import io.github.detekt.sarif4k.Level +import io.github.detekt.sarif4k.Message +import io.github.detekt.sarif4k.Result +import java.nio.file.Path + +/** + * An implementation of a [Finding] specifically for native queries. + */ +data class CpgQueryFinding( + val message: String, + val kind: Finding.Kind = Finding.Kind.Fail, + val node: Node? = null, + val relatedNodes: Collection? = null, +) { + fun toSarif(query: CPGQuery, queries: List, artifacts: Map?) = + Result( + message = Message(text = message), + kind = kind.resultKind, + level = if (kind == Finding.Kind.Fail) { + query.level + } else { + Level.None + }, + ruleIndex = queries.indexOf(query).toLong(), + locations = node?.let { listOf(node.getSarifLocation(artifacts)) }, + relatedLocations = relatedNodes?.map { node -> + node.getSarifLocation(artifacts) + } + ) +} diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryOptionGroup.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryOptionGroup.kt new file mode 100644 index 000000000..4895a3576 --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQueryOptionGroup.kt @@ -0,0 +1,27 @@ +/* + * 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.specificationLanguage.cpg.native + +import de.fraunhofer.aisec.codyze.core.executor.ExecutorOptions +import io.github.oshai.kotlinlogging.KotlinLogging + +private val logger = KotlinLogging.logger {} + +/** + * Contains all the options specific to the [CPGQueryExecutor]. For now this option group is an empty dummy. + */ +@Suppress("UNUSED") +class CPGQueryOptionGroup : ExecutorOptions("CPG Query Options") diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQuerySarifBuilder.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQuerySarifBuilder.kt new file mode 100644 index 000000000..d8a303216 --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQuerySarifBuilder.kt @@ -0,0 +1,62 @@ +/* + * 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.specificationLanguage.cpg.native + +import de.fraunhofer.aisec.codyze.core.VersionProvider +import de.fraunhofer.aisec.codyze.core.backend.Backend +import de.fraunhofer.aisec.codyze.specificationLanguage.cpg.native.queries.CPGQuery +import io.github.detekt.sarif4k.* + +private fun CPGQuery.toReportingDescriptor() = ReportingDescriptor( + id = id, + name = javaClass.simpleName, + shortDescription = MultiformatMessageString(text = shortDescription), + fullDescription = MultiformatMessageString(text = description), + defaultConfiguration = ReportingConfiguration(level = level), + help = MultiformatMessageString(text = help), + properties = + PropertyBag( + tags = tags.toSet(), + map = emptyMap() + ), +) + +class CPGQuerySarifBuilder(val queries: List, val backend: Backend) { + val reportingDescriptors = queries.map { it.toReportingDescriptor() } + val toolComponent = ToolComponent( + name = "CPGQueryExecutor", + product = "Codyze", + organization = "Fraunhofer AISEC", + semanticVersion = VersionProvider.getVersion("cpg-queries"), + downloadURI = "https://github.com/Fraunhofer-AISEC/codyze/releases", + informationURI = "https://www.codyze.io", + rules = reportingDescriptors, + ) + + fun buildRun(findings: Map>): Run { + // build the SARIF run based on the received results + return Run( + tool = Tool( + driver = toolComponent, + extensions = listOf(backend.toolInfo) + ), + artifacts = backend.artifacts.values.toList(), + results = findings.entries.flatMap { entry -> + entry.value.map { it.toSarif(entry.key, queries, backend.artifacts) } + } + ) + } +} diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQuerySubcommand.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQuerySubcommand.kt new file mode 100644 index 000000000..41b0e4367 --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/CPGQuerySubcommand.kt @@ -0,0 +1,35 @@ +/* + * 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.specificationLanguage.cpg.native + +import com.github.ajalt.clikt.parameters.groups.provideDelegate +import de.fraunhofer.aisec.codyze.backends.cpg.CPGBackend +import de.fraunhofer.aisec.codyze.core.backend.Backend +import de.fraunhofer.aisec.codyze.core.executor.ExecutorCommand + +@Suppress("UNUSED") +class CPGQuerySubcommand : ExecutorCommand("runNativeQueries") { + val executorOptions by CPGQueryOptionGroup() + + init { + // allow only the backends that implement the [CokoBackend] interface as subcommands + registerBackendOptions() + } + + override fun getExecutor(goodFindings: Boolean, pedantic: Boolean, backend: Backend?) = with(executorOptions) { + CPGQueryExecutor(CPGQueryConfiguration(true), backend as CPGBackend) + } +} diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/Formatter.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/Formatter.kt new file mode 100644 index 000000000..d501d191f --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/Formatter.kt @@ -0,0 +1,20 @@ +/* + * 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.specificationLanguage.cpg.native + +open abstract class Formatter { + public abstract fun format(k: String, v: String, attributes: Map): String +} diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/InformationExtractor.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/InformationExtractor.kt new file mode 100644 index 000000000..d30fe9a12 --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/InformationExtractor.kt @@ -0,0 +1,51 @@ +/* + * 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.specificationLanguage.cpg.native + +import de.fraunhofer.aisec.cpg.TranslationResult +import java.io.PrintStream + +open abstract class InformationExtractor { + + // Todo has a function that is executed to extract information on a TranslationResult + // Todo Has a Function that invokes a formatter and returns a string + // Todo Has a function that feeds the generated string into an output backend + + /** + * Extracts the extractor specific information from the translation result and saves it internally. + * + * The information can then be further used or formatted and printed + */ + public abstract fun extractInformation(result: TranslationResult) + + public fun printInformation(formatter: Formatter, printerF1: PrintStream, printerF2: PrintStream) { + printerF1.print(formatSFInformation(formatter)) + printerF2.print(formatTSFIInformation(formatter)) + } + + /** + * The extractor specific information needs to be given to the formatter in a key, value base fashion. The + * implementation of this function can nest key values, generated lists etc. + */ + protected abstract fun formatTSFIInformation(formatter: Formatter): String + + /** + * The extractor specific information needs to be given to the formatter in a key, value base fashion. The + * implementation of this function can nest key values, generated lists etc. + * + */ + protected abstract fun formatSFInformation(formatter: Formatter): String +} diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/TSFIInformationExtractor.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/TSFIInformationExtractor.kt new file mode 100644 index 000000000..fa3865f26 --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/TSFIInformationExtractor.kt @@ -0,0 +1,448 @@ +/* + * 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.specificationLanguage.cpg.native + +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.EnumConstantDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.* +import de.fraunhofer.aisec.cpg.graph.types.Type +import io.github.oshai.kotlinlogging.KotlinLogging +import org.xml.sax.InputSource +import java.io.StringReader +import java.io.StringWriter +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + +class TSFIInformationExtractor : InformationExtractor() { + + private val logger = KotlinLogging.logger {} + + val securityFunctionMap: MutableMap = mutableMapOf() + + val securityBehavior: MutableSet = mutableSetOf() + + val tsfiDeclarations: MutableList = mutableListOf() + + /** + * Reverse map to more efficiently find SFs related to an appearance of an action. + */ + val actionsToSFReverseMap: MutableMap> = mutableMapOf() + + override fun extractInformation(result: TranslationResult) { + val annotatedNode = result.allChildren({ it.annotations.isNotEmpty() }) + val sfs = annotatedNode.filter { it.annotations.any { it.name.localName == "SF" } } + .flatMap { it.allChildren() } + val securityBs = annotatedNode.filter { it.annotations.any { it.name.lastPartsMatch(Name("TSFI.Behavior")) } } + .flatMap { it.allChildren() } + val sfObjectives = annotatedNode.filter { it.annotations.any { it.name.toString().endsWith("SF.Objectives") } } + val sfRequirements = annotatedNode.filter { + it.annotations.any { + it.name.toString().endsWith( + "SF.Requirements" + ) + } + } + val sfActions = annotatedNode.filter { + it.annotations.any { + it.name.toString().endsWith( + "SF.SecurityActions" + ) + } + } + // val securityBehavior = annotatedNode.filter { it.annotations.any {it.name.endsWith("TSFI.Behavior") } }.flatMap { it.allChildren() } + + for (sf in sfs) { + if (!securityFunctionMap.contains(sf.name)) { + securityFunctionMap.put( + sf.name, + SecurityFunction( + sf.name, + removeAtLines((sf.comment ?: "").trimIndent().trim()).replace("\n", ""), + mutableSetOf(), + mutableSetOf(), + mutableSetOf() + ) + ) + } + } + + for (securityB in securityBs) { + securityBehavior.add(securityB.name.toString()) + } + + // Objectives + sfObjectives.forEach { + val sfToObjectives: List = it.allChildren { + it.astChildren.any { child -> + securityFunctionMap.keys.any { it.localName == child.name.localName } + } + } + sfToObjectives.forEach { parent -> + parent.astChildren.filter { it is Reference }.firstOrNull()?.let { sf -> + val secF = securityFunctionMap.keys.first { it.localName == sf.name.localName } + securityFunctionMap[secF]?.objectives?.addAll( + parent.allChildren>().map { it.value ?: "" } + ) + } + } + } + + // Requirements + sfRequirements.forEach { + val sfToRequirements: List = it.allChildren { + it.astChildren.any { child -> + securityFunctionMap.keys.any { it.localName == child.name.localName } + } + } + sfToRequirements.forEach { parent -> + parent.astChildren.filter { it is Reference }.firstOrNull()?.let { sf -> + val secF = securityFunctionMap.keys.first { it.localName == sf.name.localName } + securityFunctionMap[secF]?.requirements?.addAll( + parent.allChildren>().map { it.value ?: "" } + ) + } + } + } + + + // Actions + sfActions.forEach { + val sfToActions: List = it.allChildren { + it.astChildren.any { child -> + securityFunctionMap.keys.any { it.localName == child.name.localName } + } + } + sfToActions.forEach { parent -> + parent.astChildren.filter { it is Reference }.firstOrNull()?.let { sf -> + val secF = securityFunctionMap.keys.first { it.localName == sf.name.localName } + securityFunctionMap[secF]?.actions?.addAll( + parent.allChildren>().map { Name(it.value ?: "") } + ) + } + } + } + + securityFunctionMap.values.forEach { sf -> + sf.actions.forEach { actionName -> + if (!actionsToSFReverseMap.contains(actionName)) { + actionsToSFReverseMap[actionName] = mutableSetOf(sf) + } else { + actionsToSFReverseMap[actionName]?.add(sf) + } + } + } + + val tsfis = annotatedNode.filter { it.annotations.any { it.name.lastPartsMatch(Name("TSFI")) } } + var preSF = 0 + var postSF = 0 + var identifiedSF = 0 + + for (tsfi in tsfis) { + val tsfiSFs: MutableSet = mutableSetOf() + var behavior: Name? = null + val tsfiAnnotation = tsfi.annotations.filter { it.name.lastPartsMatch(Name("TSFI")) }.first() + + val annotationExtendedNode = mutableSetOf(tsfi) + if (tsfi is FunctionDeclaration) { + tsfi.definition?.let { annotationExtendedNode.add(it) } + } + + if (tsfi is FieldDeclaration) { + tsfi.definition.let { annotationExtendedNode.add(it) } + } + + tsfiAnnotation.members.forEach { + if (it.value is MemberExpression) { + behavior = Name(it.value?.code.toString() ?: it.name.toString()) + } else { + tsfiSFs.addAll( + it.value.allChildren().map { Name(it.code.toString()) }.toMutableSet() + ) + } + + } + + if (tsfiSFs.isEmpty()) { + val calls = annotationExtendedNode.flatMap { reachableCalls(it) } + calls.forEach { call -> + actionsToSFReverseMap.keys.forEach { actionKey -> + if (call.name.toString().startsWith(actionKey.toString())) { + tsfiSFs.addAll(actionsToSFReverseMap[actionKey]?.map { it.name } ?: setOf()) + } + } + } + if (tsfiSFs.isEmpty()) { + // TODO Here we could investigate why we do not find the SF for it. + } else { + postSF++ + } + } else { + preSF++ + } + + val description = annotationExtendedNode.map { removeAtLines(it.comment?:"") }.joinToString("").trimIndent().trim().replace("\n","") + val andContainedFunctions = annotationExtendedNode + annotationExtendedNode.flatMap { it.allChildren() } + + val relevantFunctions = andContainedFunctions.filterIsInstance().filter { !it.isInferred && !it.isImplicit }.toSet() + + val params = relevantFunctions.flatMap { parseParameters(it).values }.toMutableSet() + val exceptions = parseExceptions(relevantFunctions).values.toMutableSet() + val tsfiDeclaration = TSFI(description, behavior?:Name(""), tsfi, annotationExtendedNode, + tsfiSFs.flatMap { tsfiSFName -> + securityFunctionMap.entries.filter { it.key.toString().substringAfterLast(".") == + tsfiSFName.toString().substringAfterLast(".") }.map { it.value } }.toMutableSet(), params, exceptions) + tsfiDeclarations.add(tsfiDeclaration) + } + logger.info { "TSFI: ${tsfiDeclarations.size}: $preSF security functions explicitly specified, $postSF security functions identified through analysis." } + + } + + private fun parseParameters(function:FunctionDeclaration): Map{ + val parameterData = mutableMapOf() + for (param in function.parameters){ + parameterData.put(param.name.localName, Parameter(param.name.localName, param.type, "")) + } + function.comment?.lines()?.forEach { + val paramComment = it.substringAfter("@param").trim() + val name = paramComment.substringBefore(" ") + val description = paramComment.substringAfter(" ") + parameterData.get(name)?.description = description.trim() + } + return parameterData + } + + private fun parseExceptions(functions:Set): Map { + val exceptionsData = mutableMapOf() + + functions.forEach {function -> + function.throwsTypes.forEach { + exceptionsData.put(it, TSFIException(it, "", "")) + } + + if(exceptionsData.isNotEmpty()){ + function.comment?.lines()?.forEach { + if(it.contains("@throws")){ + val paramComment = it.substringAfter("@throws").trim() + val name = paramComment.substringBefore(" ").trim() + val description = paramComment.substringAfter(" ").trim() + exceptionsData.entries.filter { it.key.name.localName == name }.forEach { + it.value.description = description + } + } + } + } + function.allChildren().filter { it.operatorCode == "throw" }.forEach { + it.followPrevEOGEdgesUntilHit { it is ConstructExpression }.fulfilled.flatten().filterIsInstance().forEach { + val instantiates = it.type + val msg = it.arguments.firstOrNull()?.evaluate() + val typeBasedMsg = instantiates.name.localName + if(msg != null) (": $msg") else "" + + var tsfiException:TSFIException? = null + if(exceptionsData.contains(instantiates)){ + tsfiException = exceptionsData[instantiates] + }else{ + tsfiException = getTSFIFromTypeHierarchy(instantiates,exceptionsData) + } + + if(tsfiException != null){ + if(!tsfiException.message.contains(typeBasedMsg)){ + tsfiException.message += (if(tsfiException.message.isNotEmpty()) "; " else "") + typeBasedMsg + } + }else{ + // Here we could use the message as a description instead of leaving it empty + tsfiException = TSFIException(instantiates, typeBasedMsg, "") + exceptionsData.put(instantiates,tsfiException) + } + } + } + } + + return exceptionsData + } + + private fun removeAtLines(text: String): String{ + return text.lines().filter { line -> !line.contains("@param") && !line.contains("@return") && !line.contains("@throws") }.joinToString("\n") + } + + private fun getTSFIFromTypeHierarchy(type: Type, exceptionsData: Map): TSFIException?{ + if(exceptionsData.isEmpty()) + return null + var tsfiException: TSFIException? = null + var hierarchyTypes = setOf(type) + while (hierarchyTypes.isNotEmpty()){ + + hierarchyTypes.forEach {currentType -> + tsfiException = exceptionsData.entries.filter { it.key.name.localName == currentType.name.localName }.map { it.value }.firstOrNull() + if(tsfiException != null) + return tsfiException + } + // BFS in the hierarchy + hierarchyTypes = hierarchyTypes.flatMap { it.superTypes }.toSet() + } + return tsfiException + } + + private fun reachableCalls(start:Node):Set{ + val reachable = mutableSetOf(start) + val worklist = mutableListOf(start) + val eogStarters = start.allEOGStarters + worklist.addAll(eogStarters) + reachable.addAll(eogStarters) + while (worklist.isNotEmpty()){ + val current = worklist.removeFirst() + val nextNodes = mutableSetOf() + nextNodes.addAll(current.nextEOG) + if(current is CallExpression){ + nextNodes.addAll(current.invokes) + } + nextNodes.removeIf { reachable.contains(it) } + worklist.addAll(nextNodes) + reachable.addAll(nextNodes) + } + return reachable.filterIsInstance().toSet() + } + + + override fun formatSFInformation(formatter: Formatter): String { + var xml = "" + + for (sf in securityFunctionMap.values){ + var sfContent = "" + sfContent += formatter.format("description", sf.description, mapOf()) + sfContent += formatter.format("rational", "", mapOf()) + + var content = "" + for (obj in sf.objectives){ + content += formatter.format("ref","", mapOf("target" to obj)) + } + sfContent += formatter.format("objectives", content, mapOf()) + + + content = "" + for (obj in sf.requirements){ + content += formatter.format("ref","", mapOf("target" to obj)) + } + sfContent += formatter.format("requirements", content, mapOf()) + + xml += formatter.format("security-function", sfContent, mapOf("id" to replaceSFName(sf.name.toString()))) + } + + + return prettyPrint(formatter.format("security-specification", xml, mapOf()),2,true) + } + + override fun formatTSFIInformation(formatter: Formatter): String { + var xml = "" + for (tsfi in tsfiDeclarations.sortedBy { getComplexName(it.annotated) }) { + var tsfiContent = formatter.format("description", tsfi.description, mapOf()) + + var parametersContent = "" + for(param in tsfi.params){ + parametersContent += formatter.format("parameter", param.description, mapOf("name" to param.name, "type" to param.type.name.toString().replace("<", "<").replace(">", ">").replace("[]", "Array"))) + } + + if(parametersContent.isEmpty()) parametersContent = " " + tsfiContent += formatter.format("parameters", parametersContent, mapOf()) + + var errorsContent = "" + for(excepts in tsfi.exceptions){ + var errorContent = "" + errorContent += formatter.format("message", (if(excepts.message.isNotEmpty()) excepts.message else excepts.type.name.toString()).replace("<", "<").replace(">", ">"), mapOf()) + errorContent += formatter.format("effect", excepts.description, mapOf()) + + errorsContent += formatter.format("error", errorContent, mapOf()) + } + + if(errorsContent.isEmpty()) errorsContent = " " + tsfiContent += formatter.format("errors", errorsContent, mapOf()) + + + var afContent = "" + for(sf in tsfi.sf){ + afContent += formatter.format("ref", "", mapOf("target" to replaceSFName(sf.name.toString()))) + } + if(afContent.isEmpty()) afContent = " " + + tsfiContent += formatter.format("security-functions", afContent, mapOf()) + + + var actionsContent = "" + if(actionsContent.isEmpty()) actionsContent = " " + + tsfiContent += formatter.format("actions", actionsContent, mapOf()) + + + xml += formatter.format("tsfi", tsfiContent, mapOf("id" to getComplexName(tsfi.annotated), "security" to tsfi.securityBehavior.localName.substringAfterLast(".").lowercase())) + } + + + return prettyPrint(formatter.format("functional-specification", xml, mapOf()),2,true) + } + + private fun getComplexName(node: Node): String { + val nameString: String + if (node is FunctionDeclaration){ + nameString = node.name.toString() + "-" +node.parameters.map { + it.type.name.localName.replace("[]","Array").replace("<", "-lt-").replace(">", "-gt-") + }.joinToString("-") + }else{ + nameString = node.name.toString() + } + return nameString.replace("<", "-lt-").replace(">", "-gt-") + } + + private fun replaceSFName(name:String): String{ + var sfname = name.substringAfterLast("de.fraunhofer.aisec.codyze.").replace(".TOEDefinitions.SecurityFunction","") + return sfname.lowercase() + } + private fun prettyPrint(xmlString: String, indent: Int, ignoreDeclaration: Boolean):String{ + try { + val src = InputSource(StringReader(xmlString)); + val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(src); + + val transformerFactory = TransformerFactory.newInstance(); + transformerFactory.setAttribute("indent-number", indent); + val transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, if(ignoreDeclaration) "yes" else "no"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + + val out = StringWriter(); + transformer.transform(DOMSource(document), StreamResult(out)); + return out.toString(); + } catch (e:Exception) { + throw RuntimeException("Error occurs when pretty-printing xml:\n" + xmlString, e); + } + } + + /** + * In contrast to the annotation the data class does not contain the security function.When the TSFIs are parsed, the + * associated security functions are either provided in the annotation or identified through static code analysis. + */ + data class TSFI(val description: String, val securityBehavior:Name, val annotated:Node, val functions:Set, val sf:MutableSet, val params: MutableSet, val exceptions:MutableSet) + + data class Parameter(val name: String, val type:Type, var description: String) + + data class TSFIException(val type:Type, var message: String, var description: String) + + data class SecurityFunction(val name:Name, val description:String, val objectives:MutableSet, val requirements: MutableSet, val actions: MutableSet) +} diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/XMLFormatter.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/XMLFormatter.kt new file mode 100644 index 000000000..67a114175 --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/XMLFormatter.kt @@ -0,0 +1,32 @@ +/* + * 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.specificationLanguage.cpg.native + +class XMLFormatter : Formatter() { + override fun format(k: String, v: String, attributes: Map): String { + var output = "<" + k + for (att in attributes.keys) { + output += " " + att + "=\"" + attributes[att] + "\"" + } + if (v.isNotEmpty()) { + output += ">" + v + "" + } else { + output += " />" + } + + return output // TODO Replace this with an xml output generator that has some injection mitigations + } +} diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/queries/CPGQuery.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/queries/CPGQuery.kt new file mode 100644 index 000000000..44e5cfb5f --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/queries/CPGQuery.kt @@ -0,0 +1,32 @@ +/* + * 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.specificationLanguage.cpg.native.queries + +import de.fraunhofer.aisec.codyze.backends.cpg.CPGBackend +import de.fraunhofer.aisec.codyze.specificationLanguage.cpg.native.CpgQueryFinding +import io.github.detekt.sarif4k.Level + +open abstract class CPGQuery { + + abstract val id: String + abstract val shortDescription: String + abstract val description: String + val level: Level = Level.Note + var help: String = "" + val tags: Set = mutableSetOf() + + abstract fun query(backend: CPGBackend): List +} diff --git a/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/queries/ExampleQuery.kt b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/queries/ExampleQuery.kt new file mode 100644 index 000000000..91b884b90 --- /dev/null +++ b/codyze-specification-languages/cpg-native/src/main/kotlin/de/fraunhofer/aisec/codyze/specificationLanguage/cpg/native/queries/ExampleQuery.kt @@ -0,0 +1,43 @@ +/* + * 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.specificationLanguage.cpg.native.queries + +import de.fraunhofer.aisec.codyze.backends.cpg.CPGBackend +import de.fraunhofer.aisec.codyze.specificationLanguage.cpg.native.CpgQueryFinding +import de.fraunhofer.aisec.cpg.graph.Annotation +import de.fraunhofer.aisec.cpg.query.all + +class ExampleQuery : CPGQuery() { + + override val id: String = "0" + override val shortDescription: String = "A short Query Example" + override val description: String = "This query is an example of a native cpg query that can use the cpg structure to " + + "identify relevant nodes" + val message = "An implementation for cryptographic functionality was found: " + val crypoUses: List = listOf("ENCRYPT", "DECRYPT", "SIGN", "VERIFY", "RANDOM", "RNG", "RAND") + + override fun query(backend: CPGBackend): List { + val findings: MutableList = mutableListOf() + + backend.cpg.all + { crypoUses.contains(it.name.localName.toUpperCase()) } + .second.forEach { + findings.add(CpgQueryFinding(message + it.name.localName, node = it, relatedNodes = listOf(it))) + } + + return findings + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6e1037fb8..06d6c0395 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin = "1.9.24" -cpg = "8.2.0" +cpg = "8.3.0" koin = "3.5.6" koin-test = "3.5.6" detekt = "1.23.6" diff --git a/settings.gradle.kts b/settings.gradle.kts index 799329d8e..fcce7570d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,9 @@ include(":codyze-backends:cpg") include(":codyze-specification-languages:coko:coko-core") include(":codyze-specification-languages:coko:coko-dsl") +// Including the nativ cpg dsl queries +include(":codyze-specification-languages:cpg-native") + /* * Optional and experimental features */