diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 59cb7556af..5a84f06b67 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,3 +28,5 @@ cpg-neo4j @peckto build.gradle.kts @oxisto .github @oxisto + +cpg-language-ini @maximiliankaul diff --git a/build.gradle.kts b/build.gradle.kts index 368ea647b4..67e352fed8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -143,3 +143,9 @@ val enableJVMFrontend: Boolean by extra { enableJVMFrontend.toBoolean() } project.logger.lifecycle("JVM frontend is ${if (enableJVMFrontend) "enabled" else "disabled"}") + +val enableINIFrontend: Boolean by extra { + val enableINIFrontend: String? by project + enableINIFrontend.toBoolean() +} +project.logger.lifecycle("INI frontend is ${if (enableINIFrontend) "enabled" else "disabled"}") diff --git a/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts b/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts index 10fef182f3..f482eecd48 100644 --- a/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts @@ -12,6 +12,7 @@ val enableLLVMFrontend: Boolean by rootProject.extra val enableTypeScriptFrontend: Boolean by rootProject.extra val enableRubyFrontend: Boolean by rootProject.extra val enableJVMFrontend: Boolean by rootProject.extra +val enableINIFrontend: Boolean by rootProject.extra dependencies { if (enableJavaFrontend) { @@ -46,4 +47,8 @@ dependencies { api(project(":cpg-language-ruby")) kover(project(":cpg-language-ruby")) } + if (enableINIFrontend) { + api(project(":cpg-language-ini")) + kover(project(":cpg-language-ini")) + } } diff --git a/configure_frontends.sh b/configure_frontends.sh index 49e3233752..3fd8e946a7 100755 --- a/configure_frontends.sh +++ b/configure_frontends.sh @@ -60,3 +60,5 @@ answerRuby=$(ask "Do you want to enable the Ruby frontend? (currently $(getPrope setProperty "enableRubyFrontend" $answerRuby answerJVM=$(ask "Do you want to enable the JVM frontend? (currently $(getProperty "enableJVMFrontend"))") setProperty "enableJVMFrontend" $answerJVM +answerINI=$(ask "Do you want to enable the INI frontend? (currently $(getProperty "enableINIFrontend"))") +setProperty "enableINIFrontend" $answerINI diff --git a/cpg-language-ini/build.gradle.kts b/cpg-language-ini/build.gradle.kts new file mode 100644 index 0000000000..928bb3d0ec --- /dev/null +++ b/cpg-language-ini/build.gradle.kts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021, 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("cpg-language-ini") { + pom { + artifactId = "cpg-language-ini" + name.set("Code Property Graph - INI Frontend") + description.set("An INI configuration file frontend for the CPG") + } + } + } +} + +dependencies { + // ini4j for parsing ini files + implementation(libs.ini4j) + + // to evaluate some test cases + testImplementation(project(":cpg-analysis")) +} diff --git a/cpg-language-ini/src/main/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileFrontend.kt b/cpg-language-ini/src/main/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileFrontend.kt new file mode 100644 index 0000000000..340f839806 --- /dev/null +++ b/cpg-language-ini/src/main/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileFrontend.kt @@ -0,0 +1,163 @@ +/* + * 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.cpg.frontend.configfiles + +import de.fraunhofer.aisec.cpg.TranslationContext +import de.fraunhofer.aisec.cpg.frontends.Language +import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend +import de.fraunhofer.aisec.cpg.frontends.TranslationException +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration +import de.fraunhofer.aisec.cpg.graph.types.Type +import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation +import de.fraunhofer.aisec.cpg.sarif.Region +import java.io.File +import java.io.FileInputStream +import java.net.URI +import org.ini4j.Ini +import org.ini4j.Profile + +/** + * The INI file frontend. This frontend utilizes the [ini4j library](https://ini4j.sourceforge.net/) + * to parse the config file. The result consists of + * - a [TranslationUnitDeclaration] wrapping the entire result + * - a [de.fraunhofer.aisec.cpg.graph.declarations.NamespaceDeclaration] wrapping the INI file and + * thus preventing collisions with other symbols which might have the same name + * - a [RecordDeclaration] per `Section` (a section refers to a block of INI values marked with a + * line `[SectionName]`) + * - a [de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration] per entry in a section. The + * [de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration.name] matches the `entry`s `name` + * field and the [de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration.initializer] is set + * to a [statements.expressions.Literal] with the corresponding `entry`s `value`. + * + * Note: + * - the "ini4j" library does not provide any super type for all nodes. Thus, the frontend accepts + * `Any` + * - [typeOf] has to be implemented, but as there are no types always returns the builtin `string` + * type + * - [codeOf] has to accept `Any` (because of the limitations stated above) and simply returns + * `.toString()` + * - [locationOf] always returns `null` as the "ini4j" library does not provide any means of getting + * a location given a node + * - [setComment] not implemented as this is not used (no + * [de.fraunhofer.aisec.cpg.frontends.Handler] pattern implemented) + * - Comments in general are not supported. + */ +class IniFileFrontend(language: Language, ctx: TranslationContext) : + LanguageFrontend(language, ctx) { + + private lateinit var uri: URI + private lateinit var region: Region + + override fun parse(file: File): TranslationUnitDeclaration { + uri = file.toURI() + region = Region() + + val ini = Ini() + try { + ini.load(FileInputStream(file)) + } catch (ex: Exception) { + throw TranslationException("Parsing failed with exception: $ex") + } + + /* + * build a namespace name relative to the configured + * [de.fraunhofer.aisec.cpg.TranslationConfiguration.topLevel] using + * [Language.namespaceDelimiter] as a separator + */ + val topLevel = config.topLevel?.let { file.relativeToOrNull(it) } ?: file + val parentDir = topLevel.parent + + val namespace = + if (parentDir != null) { + val pathSegments = parentDir.toString().split(File.separator) + (pathSegments + file.nameWithoutExtension).joinToString(language.namespaceDelimiter) + } else { + file.nameWithoutExtension + } + + val tud = newTranslationUnitDeclaration(name = file.name, rawNode = ini) + scopeManager.resetToGlobal(tud) + val nsd = newNamespaceDeclaration(name = namespace, rawNode = ini) + scopeManager.addDeclaration(nsd) + scopeManager.enterScope(nsd) + + ini.values.forEach { handleSection(it) } + + scopeManager.enterScope(nsd) + return tud + } + + /** + * Translates a `Section` into a [RecordDeclaration] and handles all `entries` using + * [handleEntry]. + */ + private fun handleSection(section: Profile.Section) { + val record = newRecordDeclaration(name = section.name, kind = "section", rawNode = section) + scopeManager.addDeclaration(record) + scopeManager.enterScope(record) + section.entries.forEach { handleEntry(it) } + scopeManager.leaveScope(record) + } + + /** + * Translates an `MutableEntry` to a new + * [de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration] with the + * [de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration.initializer] being set to the + * `entry`s value. + */ + private fun handleEntry(entry: MutableMap.MutableEntry) { + val field = + newFieldDeclaration(name = entry.key, type = primitiveType("string"), rawNode = entry) + .apply { initializer = newLiteral(value = entry.value, rawNode = entry) } + scopeManager.addDeclaration(field) + } + + override fun typeOf(type: Any?): Type { + return primitiveType("string") + } + + override fun codeOf(astNode: Any): String? { + return astNode.toString() + } + + /** + * Return the entire file as the location of any node. The parsing library in use does not + * provide more fine granular access to a node's location. + */ + override fun locationOf(astNode: Any): PhysicalLocation? { + return PhysicalLocation( + uri, + region + ) // currently, the line number / column cannot be accessed given an Ini object -> we only + // provide a precise uri + } + + override fun setComment(node: Node, astNode: Any) { + return // not used as this function does not implement [Handler] + } +} diff --git a/cpg-language-ini/src/main/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileLanguage.kt b/cpg-language-ini/src/main/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileLanguage.kt new file mode 100644 index 0000000000..3d55cb93df --- /dev/null +++ b/cpg-language-ini/src/main/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileLanguage.kt @@ -0,0 +1,50 @@ +/* + * 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.cpg.frontend.configfiles + +import de.fraunhofer.aisec.cpg.frontends.Language +import de.fraunhofer.aisec.cpg.graph.types.StringType +import de.fraunhofer.aisec.cpg.graph.types.Type +import kotlin.reflect.KClass + +/** + * A simple language representing classical [INI files](https://en.wikipedia.org/wiki/INI_file). As + * there are conflicting definitions of an INI file, we go with: + * - the file extension is `.ini` or `.conf` + * - all entries live in a unique `section` + * - all `key`s are unique per section + * - the file is accepted by the [ini4j library](https://ini4j.sourceforge.net/) + */ +class IniFileLanguage : Language() { + override val fileExtensions = listOf("ini", "conf") + override val namespaceDelimiter: String = "." // no such thing + + @Transient override val frontend: KClass = IniFileFrontend::class + override val builtInTypes: Map = + mapOf("string" to StringType("string", language = this)) // everything is a string + + override val compoundAssignmentOperators: Set = emptySet() // no such thing +} diff --git a/cpg-language-ini/src/test/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileTest.kt b/cpg-language-ini/src/test/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileTest.kt new file mode 100644 index 0000000000..b0dee09da7 --- /dev/null +++ b/cpg-language-ini/src/test/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileTest.kt @@ -0,0 +1,92 @@ +/* + * 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.cpg.frontend.configfiles + +import de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration +import de.fraunhofer.aisec.cpg.graph.get +import de.fraunhofer.aisec.cpg.graph.records +import de.fraunhofer.aisec.cpg.test.BaseTest +import de.fraunhofer.aisec.cpg.test.analyzeAndGetFirstTU +import de.fraunhofer.aisec.cpg.test.assertFullName +import de.fraunhofer.aisec.cpg.test.assertLiteralValue +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class IniFileTest : BaseTest() { + + @Test + fun testSimpleINIFile() { + val topLevel = Path.of("src", "test", "resources") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("config.ini").toFile()), topLevel, true) { + it.registerLanguage() + } + assertIs(tu) + + val namespace = tu.namespaces.firstOrNull() + assertNotNull(namespace) + assertFullName( + "config", + namespace, + "Namespace name mismatch." + ) // analyzeAndGetFirstTU does not provide the full path + + assertEquals(2, tu.records.size, "Expected two records") + + val sectionA = tu.records["SectionA"] + assertIs(sectionA) + assertEquals(2, sectionA.fields.size, "Expected two fields") + + val sectionAEntry1 = sectionA.fields["key1"] + assertIs(sectionAEntry1) + assertLiteralValue("value1", sectionAEntry1.initializer) + + val sectionAEntry2 = sectionA.fields["key2"] + assertIs(sectionAEntry2) + assertLiteralValue("value2", sectionAEntry2.initializer) + + val sectionB = tu.records["SectionB"] + assertIs(sectionB) + assertEquals(3, sectionB.fields.size, "Expected three fields") + + val sectionBEntry1 = sectionB.fields["key1"] + assertIs(sectionBEntry1) + assertLiteralValue("123", sectionBEntry1.initializer) + + val sectionBEntry2 = sectionB.fields["key2"] + assertIs(sectionBEntry2) + assertLiteralValue("1.2.3.4", sectionBEntry2.initializer) + + val sectionBEntry3 = sectionB.fields["key3"] + assertIs(sectionBEntry3) + assertLiteralValue("\"abc\"", sectionBEntry3.initializer) + } +} diff --git a/cpg-language-ini/src/test/resources/config.ini b/cpg-language-ini/src/test/resources/config.ini new file mode 100644 index 0000000000..4e2a323b5b --- /dev/null +++ b/cpg-language-ini/src/test/resources/config.ini @@ -0,0 +1,10 @@ +; An example INI file + +[SectionA] +key1 = value1 +key2 = value2 + +[SectionB] +key1 = 123 +key2 = 1.2.3.4 +key3 = "abc" \ No newline at end of file diff --git a/gradle.properties.example b/gradle.properties.example index f956f34730..34749a17ce 100644 --- a/gradle.properties.example +++ b/gradle.properties.example @@ -9,3 +9,4 @@ enableLLVMFrontend=true enableTypeScriptFrontend=true enableRubyFrontend=true enableJVMFrontend=true +enableINIFrontend=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a8a2e5a44..bb1dda77cc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,6 +42,7 @@ llvm = { module = "org.bytedeco:llvm-platform", version = "16.0.4-1.5.9"} jruby = { module = "org.jruby:jruby-core", version = "9.4.3.0" } jline = { module = "org.jline:jline", version = "3.27.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" } # test junit-params = { module = "org.junit.jupiter:junit-jupiter-params", version = "5.11.0"} diff --git a/settings.gradle.kts b/settings.gradle.kts index afe927cdee..78d929ce31 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,6 +41,10 @@ val enableJVMFrontend: Boolean by extra { val enableJVMFrontend: String? by settings enableJVMFrontend.toBoolean() } +val enableINIFrontend: Boolean by extra { + val enableINIFrontend: String? by settings + enableINIFrontend.toBoolean() +} if (enableJavaFrontend) include(":cpg-language-java") if (enableCXXFrontend) include(":cpg-language-cxx") @@ -50,3 +54,4 @@ if (enablePythonFrontend) include(":cpg-language-python") if (enableTypeScriptFrontend) include(":cpg-language-typescript") if (enableRubyFrontend) include(":cpg-language-ruby") if (enableJVMFrontend) include(":cpg-language-jvm") +if (enableINIFrontend) include(":cpg-language-ini")