diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1299bb80d1d..4dae62f6ce0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -30,4 +30,8 @@ build.gradle.kts @oxisto .github @oxisto cpg-language-ini @maximiliankaul + cpg-concepts @maximiliankaul + +codyze @fwendland +codyze-compliance @fwendland @oxisto diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 6418a1982ad..cee4efd64bb 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,6 +8,7 @@ repositories { dependencies { implementation(libs.kotlin.gradle) + implementation(libs.kotlin.serialization) implementation(libs.dokka.gradle) implementation(libs.kover.gradle) implementation(libs.spotless.gradle) diff --git a/buildSrc/src/main/kotlin/cpg.common-conventions.gradle.kts b/buildSrc/src/main/kotlin/cpg.common-conventions.gradle.kts index c3ffde19690..c4972fa575c 100644 --- a/buildSrc/src/main/kotlin/cpg.common-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/cpg.common-conventions.gradle.kts @@ -12,6 +12,7 @@ plugins { signing `maven-publish` kotlin("jvm") + kotlin("plugin.serialization") id("org.jetbrains.dokka") } diff --git a/codyze-compliance/build.gradle.kts b/codyze-compliance/build.gradle.kts new file mode 100644 index 00000000000..b7e3933c9ab --- /dev/null +++ b/codyze-compliance/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025, 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. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +plugins { + id("cpg.frontend-conventions") +} + +publishing { + publications { + named("codyze-compliance") { + pom { + artifactId = "codyze" + name.set("Codyze - Compliance Module") + description.set("The compliance module of Codyze") + } + } + } +} + +dependencies { + implementation(projects.cpgCore) + implementation(libs.clikt) + implementation(libs.kaml) +} diff --git a/codyze-compliance/src/main/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/Command.kt b/codyze-compliance/src/main/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/Command.kt new file mode 100644 index 00000000000..e67e78d4786 --- /dev/null +++ b/codyze-compliance/src/main/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/Command.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025, 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.cpg.codyze.compliance + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.groups.OptionGroup +import com.github.ajalt.clikt.parameters.groups.provideDelegate +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import kotlin.io.path.Path + +/** Options common to all subcommands. */ +class ProjectOptions : OptionGroup("Project Options:") { + val directory by option("--project-dir", help = "The project directory").default(".") +} + +/** The main `compliance` command. */ +class ComplianceCommand : CliktCommand() { + override fun run() {} +} + +/** The `scan` command. This will scan the project for compliance violations in the future. */ +class ScanCommand : CliktCommand() { + private val projectOptions by ProjectOptions() + + override fun run() { + TODO() + } +} + +/** + * The `list-security-goals` command. This will list the names of all security goals in the + * specified project. + * + * This command assumes that the project contains a folder named `security-goals` that contains YAML + * files with the security goals. + */ +class ListSecurityGoals : CliktCommand() { + private val projectOptions by ProjectOptions() + + override fun run() { + val goals = loadSecurityGoals(Path(projectOptions.directory).resolve("security-goals")) + goals.forEach { echo(it.name.localName) } + } +} + +var Command = ComplianceCommand().subcommands(ScanCommand(), ListSecurityGoals()) diff --git a/codyze-compliance/src/main/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/SecurityGoal.kt b/codyze-compliance/src/main/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/SecurityGoal.kt new file mode 100644 index 00000000000..2cc408483d8 --- /dev/null +++ b/codyze-compliance/src/main/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/SecurityGoal.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2025, 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.cpg.codyze.compliance + +import com.charleskorn.kaml.Yaml +import com.charleskorn.kaml.decodeFromStream +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.Component +import de.fraunhofer.aisec.cpg.graph.Name +import de.fraunhofer.aisec.cpg.graph.OverlayNode +import java.io.File +import java.io.InputStream +import java.nio.file.Path +import kotlinx.serialization.Contextual +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.modules.SerializersModule + +@Serializable +data class SecurityGoal( + @Serializable(with = NameSerializer::class) override var name: Name, + val description: String, + val components: List<@Contextual Component?> = listOf(), + val assumptions: List = listOf(), + val restrictions: List = listOf(), + val objectives: List, +) : OverlayNode() + +@Serializable +class SecurityObjective( + @Serializable(with = NameSerializer::class) override var name: Name, + val description: String, + val statements: List, + val components: List<@Contextual Component?> = listOf(), + val assumptions: List = listOf(), + val restrictions: List = listOf(), +) : OverlayNode() + +/** A custom serializer for the [Name] class. */ +class NameSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor(Name::class.qualifiedName!!, PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Name) { + encoder.encodeString(value.localName) + } + + override fun deserialize(decoder: Decoder): Name { + return Name(decoder.decodeString()) + } +} + +/** + * A custom serializer for the [Component] class. If the [result] is non-null, it is used to resolve + * the component name in an actual [Component] of the [result]. Otherwise, a new [Component] with + * the given name is returned. + */ +class ComponentSerializer(val result: TranslationResult?) : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor(Component::class.qualifiedName!!, PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Component) { + encoder.encodeString(value.name.toString()) + } + + override fun deserialize(decoder: Decoder): Component { + // Use the context to find the component by name + val componentName = decoder.decodeString() + + return if (result != null) { + result.components.first { it.name.localName == componentName } + } else { + Component().also { it.name = Name(componentName) } + } + } +} + +/** + * Load all security goals from a directory. If a [result] is given, it will be used to resolve + * component names. + */ +fun loadSecurityGoals(directory: Path, result: TranslationResult? = null): List { + // Walk the directory and load all YAML files + return directory + .toFile() + .walk() + .filter { it.extension == "yaml" } + .toList() + .map { loadSecurityGoal(it, result) } +} + +/** + * Load a single security goal from a file. If a [result] is given, it will be used to resolve + * component names. + */ +fun loadSecurityGoal(file: File, result: TranslationResult? = null): SecurityGoal { + return yaml(result).decodeFromString(file.readText()) +} + +/** + * Load a single security goal from an input stream. If a [result] is given, it will be used to + * resolve component names. + */ +fun loadSecurityGoal(stream: InputStream, result: TranslationResult? = null): SecurityGoal { + return yaml(result).decodeFromStream(stream) +} + +/** + * This function returns a [Yaml] instance that is configured to use the given [result] to resolve + * components. + */ +private fun yaml(result: TranslationResult?): Yaml { + val module = SerializersModule { contextual(Component::class, ComponentSerializer(result)) } + return Yaml(serializersModule = module) +} diff --git a/codyze-compliance/src/test/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/CommandTest.kt b/codyze-compliance/src/test/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/CommandTest.kt new file mode 100644 index 00000000000..f953c62f3b9 --- /dev/null +++ b/codyze-compliance/src/test/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/CommandTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025, 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.cpg.codyze.compliance + +import com.github.ajalt.clikt.testing.test +import kotlin.test.* + +class CommandTest { + + @Test + fun testComplianceCommand() { + val command = ComplianceCommand() + val result = command.test() + assertEquals(0, result.statusCode) + } + + @Test + fun testListSecurityGoalsCommand() { + val command = ListSecurityGoals() + val result = command.test("--project-dir src/test/resources/") + assertEquals(0, result.statusCode) + assertEquals("Goal1\n", result.stdout) + } + + @Test + fun testScanCommand() { + val command = ScanCommand() + val ex = assertFails { + val result = command.test("--project-dir src/test/resources/") + assertEquals(0, result.statusCode) + } + assertIs(ex) + } +} diff --git a/codyze-compliance/src/test/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/SecurityGoalTest.kt b/codyze-compliance/src/test/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/SecurityGoalTest.kt new file mode 100644 index 00000000000..cf6483c854c --- /dev/null +++ b/codyze-compliance/src/test/kotlin/de/fraunhofer/aisec/cpg/codyze/compliance/SecurityGoalTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025, 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.cpg.codyze.compliance + +import de.fraunhofer.aisec.cpg.ScopeManager +import de.fraunhofer.aisec.cpg.TranslationConfiguration +import de.fraunhofer.aisec.cpg.TranslationContext +import de.fraunhofer.aisec.cpg.TranslationManager +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.TypeManager +import de.fraunhofer.aisec.cpg.graph.Component +import de.fraunhofer.aisec.cpg.graph.Name +import kotlin.io.path.Path +import kotlin.test.* + +class SecurityGoalTest { + @Test + fun testLoadSecurityGoals() { + val goals = loadSecurityGoals(Path("src/test/resources/security-goals")) + val goal1 = goals.firstOrNull() + assertNotNull(goal1) + assertEquals("Goal1", goal1.name.localName) + assertEquals("Make it very secure", goal1.description) + + val objective1 = goal1.objectives.firstOrNull() + assertNotNull(objective1) + assertEquals("Good encryption", objective1.name.localName) + assertEquals("Encryption used is very good", objective1.description) + } + + @Test + fun testLoadSecurityGoal() { + val stream = this::class.java.getResourceAsStream("/security-goals/goal1.yaml") + assertNotNull(stream) + + val goal1 = loadSecurityGoal(stream) + assertNotNull(goal1) + assertEquals("Goal1", goal1.name.localName) + assertEquals("Make it very secure", goal1.description) + + val objective1 = goal1.objectives.firstOrNull() + assertNotNull(objective1) + assertEquals("Good encryption", objective1.name.localName) + assertEquals("Encryption used is very good", objective1.description) + } + + @Test + fun testLoadSecurityGoalWithResult() { + val stream = this::class.java.getResourceAsStream("/security-goals/goal1.yaml") + assertNotNull(stream) + + val result = + TranslationResult( + translationManager = TranslationManager.builder().build(), + TranslationContext( + config = TranslationConfiguration.builder().build(), + scopeManager = ScopeManager(), + typeManager = TypeManager(), + ), + ) + val auth = Component().also { it.name = Name("auth") } + result.components += auth + val webserver = Component().also { it.name = Name("webserver") } + result.components += webserver + + val goal1 = loadSecurityGoal(stream, result) + assertNotNull(goal1) + assertEquals(listOf(auth, webserver), goal1.components) + + val objective1 = goal1.objectives.firstOrNull() + assertNotNull(objective1) + assertEquals(listOf(auth), objective1.components) + } +} diff --git a/codyze-compliance/src/test/resources/security-goals/goal1.yaml b/codyze-compliance/src/test/resources/security-goals/goal1.yaml new file mode 100644 index 00000000000..767f8973dd9 --- /dev/null +++ b/codyze-compliance/src/test/resources/security-goals/goal1.yaml @@ -0,0 +1,14 @@ +name: Goal1 +description: Make it very secure +components: + - auth + - webserver +assumptions: + - Third party code is very good +objectives: + - name: Good encryption + description: Encryption used is very good + statements: + - For each algorithm A, if A is used, then A must be a very good cryptographic algorithm + components: + - auth diff --git a/codyze/build.gradle.kts b/codyze/build.gradle.kts new file mode 100644 index 00000000000..e3bfa2fd521 --- /dev/null +++ b/codyze/build.gradle.kts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025, 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. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +plugins { + id("cpg.application-conventions") + id("cpg.frontend-dependency-conventions") +} + +application { + mainClass.set("de.fraunhofer.aisec.cpg.codyze.ApplicationKt") +} + +publishing { + publications { + named("codyze") { + pom { + artifactId = "codyze" + name.set("Codyze") + description.set("The one-stop shop to the code property graph") + } + } + } +} + +dependencies { + implementation(libs.clikt) + implementation(project(":codyze-compliance")) +} diff --git a/codyze/src/main/kotlin/de/fraunhofer/aisec/cpg/codyze/Application.kt b/codyze/src/main/kotlin/de/fraunhofer/aisec/cpg/codyze/Application.kt new file mode 100644 index 00000000000..23764bfb0dd --- /dev/null +++ b/codyze/src/main/kotlin/de/fraunhofer/aisec/cpg/codyze/Application.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025, 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.cpg.codyze + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.main +import com.github.ajalt.clikt.core.subcommands + +class Codyze : CliktCommand() { + override fun run() {} +} + +fun main(args: Array) { + Codyze().subcommands(de.fraunhofer.aisec.cpg.codyze.compliance.Command).main(args) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f43fae63e12..740fa717065 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,8 @@ spotless = "7.0.1" nexus-publish = "2.0.0" sootup = "1.3.0" slf4j = "2.0.16" +clikt = "5.0.2" +kaml = "0.67.0" [libraries] kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin"} @@ -48,6 +50,8 @@ jruby = { module = "org.jruby:jruby-core", version = "9.4.3.0" } jline = { module = "org.jline:jline", version = "3.28.0" } antlr-runtime = { module = "org.antlr:antlr4-runtime", version = "4.8-1" } # we cannot upgrade until ki-shell upgrades this! ini4j = { module = "org.ini4j:ini4j", version = "0.5.4" } +clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } +kaml = { module = "com.charleskorn.kaml:kaml", version.ref = "kaml" } # test junit-params = { module = "org.junit.jupiter:junit-jupiter-params", version = "5.11.0"} @@ -55,6 +59,7 @@ mockito = { module = "org.mockito:mockito-core", version = "5.15.2"} # plugins needed for build.gradle.kts in buildSrc kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "2.0.0" } # the dokka plugin is slightly behind the main Kotlin release cycle dokka-versioning = { module = "org.jetbrains.dokka:versioning-plugin", version = "2.0.0"} kover-gradle = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version = "0.9.0" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 91edf9c911e..a201d4f3ca2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,9 @@ include(":cpg-neo4j") include(":cpg-console") include(":cpg-concepts") +include(":codyze") +include(":codyze-compliance") + // this code block also exists in the root build.gradle.kts val enableJavaFrontend: Boolean by extra { val enableJavaFrontend: String? by settings