diff --git a/cpg-analysis/build.gradle.kts b/cpg-analysis/build.gradle.kts index 201e7932b3..570e2b72e9 100644 --- a/cpg-analysis/build.gradle.kts +++ b/cpg-analysis/build.gradle.kts @@ -24,6 +24,7 @@ * */ plugins { + id("cpg.application-conventions") id("cpg.library-conventions") } @@ -40,8 +41,22 @@ publishing { } } +application { + mainClass.set("de.fraunhofer.aisec.cpg.query.RuleRunnerKt") +} + dependencies { api(projects.cpgCore) + api(projects.cpgLanguageCxx) + api(projects.cpgLanguageJava) + // api(projects.cpgLanguageGo) + + implementation(libs.sarif4k) + + // Command line interface support + api(libs.picocli) + annotationProcessor(libs.picocli.codegen) + testImplementation(testFixtures(projects.cpgCore)) } diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Query.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Query.kt index 0f4fd3a68e..5ab9cc1fb9 100644 --- a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Query.kt +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Query.kt @@ -30,10 +30,7 @@ import de.fraunhofer.aisec.cpg.analysis.NumberSet import de.fraunhofer.aisec.cpg.analysis.SizeEvaluator import de.fraunhofer.aisec.cpg.analysis.ValueEvaluator import de.fraunhofer.aisec.cpg.graph.* -import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression -import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal -import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression import de.fraunhofer.aisec.cpg.graph.types.Type /** @@ -120,7 +117,7 @@ inline fun Node.exists( */ fun sizeof(n: Node?, eval: ValueEvaluator = SizeEvaluator()): QueryTree { // The cast could potentially go wrong, but if it's not an int, it's not really a size - return QueryTree(eval.evaluate(n) as? Int ?: -1, mutableListOf(), "sizeof($n)") + return QueryTree(eval.evaluate(n) as? Int ?: -1, mutableListOf(QueryTree(n)), "sizeof($n)") } /** @@ -134,7 +131,7 @@ fun min(n: Node?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree } // Extend this when we have other evaluators. } - return QueryTree(result, mutableListOf(), "min($n)") + return QueryTree(result, mutableListOf(QueryTree(n)), "min($n)") } /** @@ -176,7 +173,7 @@ fun max(n: List?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree } // Extend this when we have other evaluators. } - return QueryTree(result, mutableListOf(), "max($n)") + return QueryTree(result, mutableListOf(QueryTree(n)), "max($n)") } /** @@ -190,7 +187,7 @@ fun max(n: Node?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree { */ fun executionPath(from: Node, predicate: (Node) -> Boolean): QueryTree { val evalRes = from.followNextEOGEdgesUntilHit(predicate) - val allPaths = evalRes.fulfilled.map { QueryTree(it) }.toMutableList() - allPaths.addAll(evalRes.failed.map { QueryTree(it) }) + val allPaths = evalRes.fulfilled.map { QueryTree(it, it.map { QueryTree(it) }.toMutableList()) } + // allPaths.addAll(evalRes.failed.map { QueryTree(it) }) return QueryTree( evalRes.fulfilled.isNotEmpty(), allPaths.toMutableList(), @@ -320,91 +317,3 @@ val Expression.size: QueryTree get() { return sizeof(this) } - -/** - * The minimal integer value of this expression. It uses the default argument for `eval` of [min] - */ -val Expression.min: QueryTree - get() { - return min(this) - } - -/** - * The maximal integer value of this expression. It uses the default argument for `eval` of [max] - */ -val Expression.max: QueryTree - get() { - return max(this) - } - -/** Calls [ValueEvaluator.evaluate] for this expression, thus trying to resolve a constant value. */ -val Expression.value: QueryTree - get() { - return QueryTree(evaluate(), mutableListOf(), "$this") - } - -/** - * Calls [ValueEvaluator.evaluate] for this expression, thus trying to resolve a constant value. The - * result is interpreted as an integer. - */ -val Expression.intValue: QueryTree? - get() { - val evalRes = evaluate() as? Int ?: return null - return QueryTree(evalRes, mutableListOf(), "$this") - } - -/** - * Does some magic to identify if the value which is in [from] also reaches [to]. To do so, it goes - * some data flow steps backwards in the graph (ideally to find the last assignment) and then - * follows this value to the node [to]. - */ -fun allNonLiteralsFromFlowTo(from: Node, to: Node, allPaths: List>): QueryTree { - return when (from) { - is CallExpression -> { - val prevEdges = - from.prevDFG - .fold(mutableListOf()) { l, e -> - if (e !is Literal<*>) { - l.add(e) - } - l - } - .toMutableSet() - prevEdges.addAll(from.arguments) - // For a call, we collect the incoming data flows (typically only the arguments) - val prevQTs = prevEdges.map { allNonLiteralsFromFlowTo(it, to, allPaths) } - QueryTree(prevQTs.all { it.value }, prevQTs.toMutableList()) - } - is Literal<*> -> - QueryTree(true, mutableListOf(QueryTree(from)), "DF Irrelevant for Literal node") - else -> { - // We go one step back to see if that one goes into to but also check that no assignment - // to from happens in the paths between from and to - val prevQTs = from.prevFullDFG.map { dataFlow(it, to) }.toMutableSet() - // The base flows into a MemberExpression, but we don't care about such a partial - // flow and are only interested in the prevDFG setting the field (if it exists). So, if - // there are multiple edges, we filter out partial edges. - - val noAssignmentToFrom = - allPaths.none { - it.any { it2 -> - if (it2 is AssignmentHolder) { - it2.assignments.any { assign -> - val prevMemberFromExpr = (from as? MemberExpression)?.prevDFG - val nextMemberToExpr = (assign.target as? MemberExpression)?.nextDFG - assign.target == from || - prevMemberFromExpr != null && - nextMemberToExpr != null && - prevMemberFromExpr.any { it3 -> - nextMemberToExpr.contains(it3) - } - } - } else { - false - } - } - } - QueryTree(prevQTs.all { it.value } && noAssignmentToFrom, prevQTs.toMutableList()) - } - } -} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt index 55dabbea5e..d26543125e 100644 --- a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt @@ -157,6 +157,50 @@ open class QueryTree( } throw QueryException("Cannot compare objects of type ${this.value} and $other") } + + operator fun plus(increment: Int): QueryTree { + if (this.value is Number) { + return QueryTree( + (value as Number).toInt() + increment, + mutableListOf(this), + "$value + $increment" + ) + } + throw QueryException("Cannot add $increment to $value") + } + + operator fun minus(decrement: Int): QueryTree { + if (this.value is Number) { + return QueryTree( + (value as Number).toInt() - decrement, + mutableListOf(this), + "$value - $decrement" + ) + } + throw QueryException("Cannot subtract $decrement from $value") + } + + operator fun times(multiplier: Int): QueryTree { + if (this.value is Number) { + return QueryTree( + (value as Number).toInt() * multiplier, + mutableListOf(this), + "$value * $multiplier" + ) + } + throw QueryException("Cannot multiply $value by $multiplier") + } + + operator fun div(divisor: Int): QueryTree { + if (this.value is Number) { + return QueryTree( + (value as Number).toInt() / divisor, + mutableListOf(this), + "$value / $divisor" + ) + } + throw QueryException("Cannot divide $value by $divisor") + } } /** Performs a logical and (&&) operation between the values of two [QueryTree]s. */ diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Reporter.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Reporter.kt new file mode 100644 index 0000000000..1441c5e6a0 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Reporter.kt @@ -0,0 +1,86 @@ +/* + * 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.query + +import de.fraunhofer.aisec.cpg.query.Rule.Level +import java.nio.file.Path +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kotlin.io.path.createDirectories +import kotlin.io.path.writeText + +interface Reporter { + + /** + * Generates a report for the given rule + * + * @param rules the rule to generate the report for. The query result of the rule must be set by + * calling [Rule.run] before calling this method + * @param minify if true, a minified version of the report is generated + * @return the report as a string that can be written to a file + */ + fun report( + rules: Collection, + minify: Boolean = false, + arguments: List = ArrayList(0), + ): String + + /** + * Maps a level to the respective format + * + * @param level the level to map + * @return the mapped level + */ + fun mapLevel(level: Level): Any + + /** + * Writes the report to a file + * + * @param report the report to write to a file. Should be generated by calling [report] + * @param path the path to write the report to. If unspecified, the default path is used + */ + fun toFile(report: String, path: Path = getDefaultPath()) { + println("writing report to ${path.toAbsolutePath()}") // TODO: actual logging + path.parent.createDirectories() // create parent directories if they don't exist + path.writeText(report) + } + + /** + * Gets the default path to write the report to. Currently, the default path is + * `$(pwd)/reports/report-.sarif` + * + * @return the default path to write the report to + */ + fun getDefaultPath(): Path { + // TODO: duplicates technically possible if multiple reports are generated in the same + // second + // TODO: base path should be configurable + // eg reports/sarif/report-2021-09-29-15-00-00.sarif + val timestamp: String = + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")) + return Path.of("reports", "report-$timestamp.sarif") + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Rule.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Rule.kt new file mode 100644 index 0000000000..cf06dfec37 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Rule.kt @@ -0,0 +1,83 @@ +/* + * 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.query + +import de.fraunhofer.aisec.cpg.TranslationResult + +interface Rule { + /** the query result, one of the members has to be `null` */ + var queryResult: QueryTree<*>? /* Pair?, Pair>?>? */ + + // consider + // https://github.com/microsoft/sarif-tutorials/blob/main/docs/Authoring-rule-metadata-and-result-messages.md + // TODO: descriptive or "correct" names for the fields? + // TODO: consider metadatea structure wrt output formats (SARIF but potentially others) + // rn the fields are quite specific which might not be ideal with multiple output formats + /** stable and opaque identifier for the query */ + val id: String + + /** human readable name of the query */ + val name: String + + /** + * !!only the number!! + * + * something like "CWE-123" WILL break stuff + * + * TODO consider validation + */ + val cweId: String? + get() = null + + val shortDescription: String + val mdShortDescription: String? + get() = null + + val level: Level + val message: String? + get() = null + + val mdMessage: String? + get() = null + + val messageArguments: List? + get() = null + + /** + * executes the query on the given result. Stores the result in the [queryResult] field of the + * respective rule. Should populate the [queryResult] field. + * + * @param result the result of a translation + */ + fun run(result: TranslationResult) + + enum class Level { + Error, + Warning, + Note, + None + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/RuleRunner.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/RuleRunner.kt new file mode 100644 index 0000000000..2c27d19296 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/RuleRunner.kt @@ -0,0 +1,228 @@ +/* + * 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.query + +import de.fraunhofer.aisec.cpg.TranslationConfiguration +import de.fraunhofer.aisec.cpg.TranslationManager +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.frontends.CompilationDatabase +import de.fraunhofer.aisec.cpg.rules.* +import java.io.File +import java.nio.file.Path +import kotlin.system.exitProcess +import picocli.CommandLine + +// TODO don't hardcode this here +enum class Language { + C, + CPP, + JAVA, + LLVMIR, + PYTHON, + GOLANG, + TYPESCRIPT, + RUBY, + ALL; + + fun toClassName(): String { + return when (this) { + C -> "de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage" + CPP -> "de.fraunhofer.aisec.cpg.frontends.cxx.CPPLanguage" + JAVA -> "de.fraunhofer.aisec.cpg.frontends.java.JavaLanguage" + LLVMIR -> "de.fraunhofer.aisec.cpg.frontends.llvm.LLVMIRLanguage" + PYTHON -> "de.fraunhofer.aisec.cpg.frontends.python.PythonLanguage" + GOLANG -> "de.fraunhofer.aisec.cpg.frontends.golang.GoLanguage" + TYPESCRIPT -> "de.fraunhofer.aisec.cpg.frontends.typescript.TypeScriptLanguage" + RUBY -> "de.fraunhofer.aisec.cpg.frontends.ruby.RubyLanguage" + ALL -> "" // optionalLanguage() doesnt crash + } + } +} + +/** + * A class that runs a set of rules on a given [CompilationDatabase] and reports the results + * + * @param rules List of [Rule]s to run + * @param languages List of [Language]s to use + * @param reporter the [Reporter] to use for reporting + * @param compilationDatabase the compilation database to use + */ +class RuleRunner( + private val rules: List, + private var languages: List, + private val reporter: Reporter, + compilationDatabase: CompilationDatabase, + loadIncludes: Boolean +) { + private val config: TranslationConfiguration = + TranslationConfiguration.builder() + .useCompilationDatabase(compilationDatabase) + .sourceLocations(compilationDatabase.sourceFiles) + .let { it -> + if (languages.contains(Language.ALL)) { + languages = Language.entries.filter { inner -> inner.name != "ALL" } + } + for (language in languages) { + try { + it.registerLanguage(language.toClassName()) + } catch (_: Exception) { + // TODO: log + println( + "Failed to register language \"$language\", maybe it's not configured in " + + "gradle.properties?" + ) + } + } + it + } + .loadIncludes(loadIncludes) + .defaultPasses() + .useParallelFrontends(true) + .useParallelPasses(true) + .build() + + private val result: TranslationResult = + TranslationManager.builder().config(config).build().analyze().get() + + /** Runs the [rules] on the given [TranslationResult] */ + fun runRules() { + for (rule in rules) { + rule.run(result) + } + } + + /** + * Reports the results of the rules to a file. Uses the [reporter] to generate the report. + * + * @param minify if false, the output will not be minified and will be more human-readable + * @param path the [Path] to write the report to. If unspecified, the [reporter]'s default path + * is used + */ + fun report(minify: Boolean = true, path: Path = reporter.getDefaultPath()) { + reporter.toFile(reporter.report(rules, minify), path) + } +} + +@CommandLine.Command( + name = "cpg-analysis", + description = + [ + "Runs a set of rules on a given compilation database and reports the results. " + + "The rules are hard-coded in the source code. " + + "The output is a SARIF file as no other reporters are implemented." + ] +) +private class Cli : Runnable { + @CommandLine.Option( + names = ["-cdb", "--compilation-database"], + description = ["Path to the JSON compilation database."], + paramLabel = "FILE", + required = true + ) + lateinit var compilationDatabase: File + + private class LanguageConverter : CommandLine.ITypeConverter { + override fun convert(value: String?): Language { + return try { + Language.valueOf(value!!.uppercase()) + } catch (_: Exception) { + throw CommandLine.ParameterException(CommandLine(this), "Invalid language:$value") + } // TODO: maybe ignore or just warn + } + } + + @CommandLine.Option( + names = ["-l", "--languages"], + description = + [ + "Languages to analyze, any of {C, CPP, JAVA, LLVMIR, PYTHON, GOLANG, TYPESCRIPT, RUBY, ALL}. " + + "Case insensitive. Defaults to ALL." + ], + split = ",", + converter = [LanguageConverter::class], + defaultValue = "ALL" + ) + val languages: List = emptyList() + + @CommandLine.Option(names = ["-m", "--minify"], description = ["Minify the output."]) + var minify: Boolean = false + + @CommandLine.Option( + names = ["-o", "--output"], + description = ["Path to write the output to. If unspecified, a default path is used."], + paramLabel = "FILE" + ) + var outputPath: File? = null + + @CommandLine.Option( + names = ["--load-includes"], + description = ["Enable TranslationConfiguration option loadIncludes"] + ) + private var loadIncludes: Boolean = false + + @SuppressWarnings("unused") // used by picocli + @CommandLine.Option( + names = ["-h", "--help"], + usageHelp = true, + description = ["display this help and exit."] + ) + var help: Boolean = false + + // TODO: add functionality + // -> Pass-related options from the neo4j app + // -> don't hardcode rules but load them dynamically (may be similar to the pass system in the + // neo4j app) + + /** + * Runs the rules on the given compilation database and reports the results. + * + * The rules are currently built into the application and cannot be changed without modifying + * the source code. The output is a SARIF file because currently the only [Reporter] is the + * [SarifReporter]. The report's path is determined by the [Reporter.getDefaultPath] method of + * the respective [Reporter]. + */ + override fun run() { + val runner = + RuleRunner( + rules = + listOf( + // BufferOverreadMemcpy() + ), + languages = languages, + reporter = SarifReporter(), + compilationDatabase = CompilationDatabase.fromFile(compilationDatabase), + loadIncludes = loadIncludes + ) + runner.runRules() + if (outputPath != null) runner.report(minify = minify, path = outputPath!!.toPath()) + else runner.report(minify = minify) + } +} + +fun main(args: Array) { + val exitCode = CommandLine(Cli()).execute(*args) + exitProcess(exitCode) +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/SarifReporter.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/SarifReporter.kt new file mode 100644 index 0000000000..c494187244 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/SarifReporter.kt @@ -0,0 +1,254 @@ +/* + * 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.query + +import de.fraunhofer.aisec.cpg.graph.Node +import io.github.detekt.sarif4k.* +import java.nio.file.Paths + +class SarifReporter : Reporter { + private val pwd = Paths.get("").toAbsolutePath() + + /** + * Generates a SARIF report from a collection of rules. The Rule must have a query result set by + * calling [Rule.run] and will not be run by this method. + * + * @param rules the [Rule]s to generate the report for + * @param minify if true, the output json will be minified to reduce file size + */ + override fun report(rules: Collection, minify: Boolean, arguments: List): String { + // TODO: consider validation of rule fields + val sarifObj = + SarifSchema210( + schema = + "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json", + version = Version.The210, + runs = + listOf( + Run( + tool = + Tool( + driver = + ToolComponent( + name = + "AISEC cpg-runner", // TODO: mby dont hardcode, at + // least not here + informationURI = + "https://github.com/Fraunhofer-AISEC/cpg/", + rules = + rules.map { rule -> + ReportingDescriptor( + id = rule.id, + name = rule.name, + shortDescription = + MultiformatMessageString( + text = rule.shortDescription, + markdown = rule.mdShortDescription + ), + defaultConfiguration = + ReportingConfiguration( + level = mapLevel(rule.level) + ) + // TODO: consider default message + ) + } + ) + ), + externalPropertyFileReferences = + ExternalPropertyFileReferences( + taxonomies = + listOf( + ExternalPropertyFileReference( + location = + ArtifactLocation( + // TODO mby dont hardcode? + uri = + "https://raw.githubusercontent.com/sarif-standard/taxonomies/main/CWE_v4.4.sarif" + ) + ) + ) + ), + // TODO: heuristic for executionSuccessful needed + invocations = + listOf( + Invocation(executionSuccessful = true, arguments = arguments) + ), + results = results(rules) + ) + ) + ) + return if (minify) SarifSerializer.toMinifiedJson(sarifObj) + else SarifSerializer.toJson(sarifObj) + } + + private fun results(rules: Collection): List { + val results = mutableListOf() + for ((i, rule) in rules.withIndex()) { + results.addAll(results(rule, i.toLong())) + } + + return results + } + + private fun results(rule: Rule, idx: Long): List { + val results = mutableListOf() + for (node in rule.queryResult?.children ?: emptyList()) { + val threadFlowLocations = threadFlows(node) + results.add( + Result( + ruleID = rule.id, + ruleIndex = idx, + message = + Message( + text = rule.message, + markdown = rule.mdMessage, + arguments = rule.messageArguments + ), + taxa = + listOf( + ReportingDescriptorReference( + id = if (rule.cweId != null) "CWE-${rule.cweId}" else null + ) + ), + locations = + findReasonableLocation(threadFlowLocations).let { + if (it != null) listOf(it) else null + }, + codeFlows = codeFlows(threadFlowLocations), + ) + ) + } + return results + } + + private fun findReasonableLocation(threadFlowLocations: List): Location? { + threadFlowLocations.getOrNull(threadFlowLocations.lastIndex)?.let { + val physicalLocation = it.location?.physicalLocation + return@findReasonableLocation Location( + physicalLocation = + PhysicalLocation( + artifactLocation = + ArtifactLocation(uri = physicalLocation?.artifactLocation?.uri), + region = + Region( + startLine = physicalLocation?.region?.startLine, + endLine = physicalLocation?.region?.endLine, + startColumn = physicalLocation?.region?.startColumn, + endColumn = physicalLocation?.region?.endColumn + ) + ) + ) + } + return null + } + + private fun codeFlows(threadFlowLocations: MutableList) = + if (threadFlowLocations.isEmpty()) null + else listOf(CodeFlow(threadFlows = listOf(ThreadFlow(locations = threadFlowLocations)))) + + private fun threadFlows(root: QueryTree<*>): MutableList { + var initDepth: Long = -1 + var nodeValueLocation: de.fraunhofer.aisec.cpg.sarif.PhysicalLocation? = null + val threadFlowLocations = mutableListOf() + + root.inOrder({ (node, depth): Pair, Long> -> + if (node.value is Node) { + nodeValueLocation = (node.value as Node).location + if (nodeValueLocation != null) { + threadFlowLocations.add( + ThreadFlowLocation( + location = + Location( + physicalLocation = + PhysicalLocation( + artifactLocation = + ArtifactLocation( + uri = + nodeValueLocation + ?.artifactLocation + ?.uri + .toString() + // TODO no baseId + ), + region = + Region( + startLine = + nodeValueLocation + ?.region + ?.startLine + ?.toLong(), + endLine = + nodeValueLocation + ?.region + ?.endLine + ?.toLong(), + startColumn = + nodeValueLocation + ?.region + ?.startColumn + ?.toLong(), + endColumn = + nodeValueLocation + ?.region + ?.endColumn + ?.toLong() + ) + ) + ), + nestingLevel = + if (initDepth == -1L) { + initDepth = depth + 0 + } else { + depth - initDepth + } + // TODO: state (consider?) + ) + ) + } + } // else return empty list + }) + return threadFlowLocations + } + + private fun QueryTree<*>.inOrder(action: (Pair, Long>) -> Unit, depth: Long = 0) { + action(Pair(this, depth)) + children.forEach { it.inOrder(action, depth + 1) } + } + + override fun mapLevel(level: Rule.Level): Level { + return when (level) { + Rule.Level.Error -> Level.Error + Rule.Level.Warning -> Level.Warning + Rule.Level.Note -> Level.Note + else -> Level.None + } + } + + // dont override toFile + + // don't override getDefaultPath +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/ArrayOverRead.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/ArrayOverRead.kt new file mode 100644 index 0000000000..2b217060e2 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/ArrayOverRead.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.rules + +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.statements.expressions.SubscriptExpression +import de.fraunhofer.aisec.cpg.query.* + +class ArrayOverRead : Rule { + // cwe 126 overread + override var queryResult: QueryTree<*>? = null // to be set + override val id = "cpg-0010" // TODO IDS + override val name = "array over-read" + override val cweId: String = "126" + override val shortDescription = + "This rule detects Array accesses with indices larger than or equal to the size " + + "of the Array" + override val level = Rule.Level.Error + override val message = "Array over-read detected" + + override fun run(result: TranslationResult) { + queryResult = + result.allExtended( + mustSatisfy = { max(it.subscriptExpression) gt sizeof(it) } + ) + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/ArrayUnderRead.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/ArrayUnderRead.kt new file mode 100644 index 0000000000..0e52978ef3 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/ArrayUnderRead.kt @@ -0,0 +1,48 @@ +/* + * 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.rules + +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.statements.expressions.SubscriptExpression +import de.fraunhofer.aisec.cpg.query.* + +class ArrayUnderRead : Rule { + // cwe 127 underread + override var queryResult: QueryTree<*>? = null // to be set + override val id = "cpg-0011" // TODO IDS + override val name = "array under-read" + override val cweId: String = "127" + override val shortDescription = "This rule detects Array accesses with indices smaller than 0" + override val level = Rule.Level.Error + override val message = "Array under-read detected" + + override fun run(result: TranslationResult) { + queryResult = + result.allExtended( + mustSatisfy = { min(it.subscriptExpression) lt const(0) } + ) + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/AssignmentIntegerOverflow.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/AssignmentIntegerOverflow.kt new file mode 100644 index 0000000000..d5236a5165 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/AssignmentIntegerOverflow.kt @@ -0,0 +1,49 @@ +/* + * 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.rules + +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.statements.expressions.AssignExpression +import de.fraunhofer.aisec.cpg.query.* + +class AssignmentIntegerOverflow : Rule { + override var queryResult: QueryTree<*>? = null // to be set + override val id = "cpg-0013" // TODO IDS + override val name = "Assignment Integer Overflow" + override val cweId: String = "190" + override val shortDescription = + "Detects assignments that may cause the left-hand side to overflow" + override val level = Rule.Level.Error + override val message = "Assignment may cause overflow" + + override fun run(result: TranslationResult) { + queryResult = + result.allExtended( + { it.lhs[0].type.isPrimitive }, + { max(it.rhs[0]) gt maxSizeOfType(it.lhs[0].type) } + ) + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/BufferOverflowMemcpy.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/BufferOverflowMemcpy.kt new file mode 100644 index 0000000000..3d224b678a --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/BufferOverflowMemcpy.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.rules + +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.query.* + +class BufferOverflowMemcpy : Rule { + override var queryResult: QueryTree<*>? = null // to be setv + override val id = "cpg-0001" // TODO IDS + override val name = "memcpy dest smaller than size" + override val cweId = "787" + override val shortDescription = + "This rule detects memcpy calls where the size argument is larger than the size of the destination," + + "which can overflow the destination buffer" + override val level = Rule.Level.Error + override val message = "memcpy call with destination size than size argument detected" + + override fun run(result: TranslationResult) { + queryResult = + result.allExtended( + { it.name.localName == "memcpy" }, + { sizeof(it.arguments[0]) lt min(it.arguments[2]) } + ) + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/BufferOverreadMemcpy.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/BufferOverreadMemcpy.kt new file mode 100644 index 0000000000..8162abbcf7 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/BufferOverreadMemcpy.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.cpg.rules + +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.query.* + +class BufferOverreadMemcpy : Rule { + override var queryResult: QueryTree<*>? = null // to be set + override val id = "cpg-0002" // TODO IDS + override val name = "memcpy src smaller than size" + override val shortDescription = + "This rule detects memcpy calls where the size of the source is smaller than the size argument, which can " + + "overread the src buffer" + override val cweId: String = "787" + override val level = Rule.Level.Error + override val message = "memcpy call with source size smaller than size argument detected" + + override fun run(result: TranslationResult) { + queryResult = + result.allExtended( + { it.name.localName == "memcpy" }, + // src n + { sizeof(it.arguments[1]) le min(it.arguments[2]) } + ) + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/DoubleFree.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/DoubleFree.kt new file mode 100644 index 0000000000..dcb0d0b3b6 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/DoubleFree.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.cpg.rules + +// import de.fraunhofer.aisec.cpg.graph.statements.expressions.* // consider specifying this +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference +import de.fraunhofer.aisec.cpg.query.* + +class DoubleFree : Rule { + override var queryResult: QueryTree<*>? = null // to be set + override val id = "cpg-0016" + override val name = "Double Free" + override val cweId = "415" + override val shortDescription = "Detects double free of memory" + override val level = Rule.Level.Error + override val message = "Double free detected" + + override fun run(result: TranslationResult) { + queryResult = + result.allExtended( + { it.name.localName == "free" }, + { outer -> + not( + executionPath(outer) { inner -> + inner is CallExpression && + inner.name.localName == "free" && + ((inner.arguments[0] as Reference) + .refersTo + ?.equals((outer.arguments[0] as Reference).refersTo) ?: false) + } + ) + } + ) + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/IncrementIntegerOverflow.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/IncrementIntegerOverflow.kt new file mode 100644 index 0000000000..5313e6c6d9 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/IncrementIntegerOverflow.kt @@ -0,0 +1,70 @@ +/* + * 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.rules + +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.statements.expressions.UnaryOperator +import de.fraunhofer.aisec.cpg.graph.types.NumericType +import de.fraunhofer.aisec.cpg.query.* +import kotlin.math.pow + +class IncrementIntegerOverflow : Rule { + override var queryResult: QueryTree<*>? = null // to be set + override val id = "cpg-0014" // TODO IDS + override val name = "Increment Integer Overflow" + override val cweId: String = "128" + override val shortDescription = + "Detects (post- or prefix) unary increments that may cause the their target to " + + "overflow" + override val level = Rule.Level.Error + override val message = "Increment may cause overflow" + + override fun run(result: TranslationResult) { + queryResult = + result.allExtended( + { it.operatorCode == "++" && it.type is NumericType }, + { + val max = max(it.input) + (max eq maxSizeOfType(it.input.type)) or + when (max.value) { + is Long -> + max as QueryTree eq + const((2L shl (it.input.type as NumericType).bitWidth!!) - 1) + is Int -> + max as QueryTree eq + const((2 shl (it.input.type as NumericType).bitWidth!!) - 1) + is Float -> + max as QueryTree eq + const(2.0f.pow((it.input.type as NumericType).bitWidth!!) - 1) + is Double -> + max as QueryTree eq + const(2.0.pow((it.input.type as NumericType).bitWidth!!) - 1) + else -> const(false) + } + } + ) + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/NullPointerDereference.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/NullPointerDereference.kt new file mode 100644 index 0000000000..82806a3e7b --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/NullPointerDereference.kt @@ -0,0 +1,44 @@ +/* + * 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.rules + +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.HasBase +import de.fraunhofer.aisec.cpg.query.* + +class NullPointerDereference : Rule { + // cwe 476 + override var queryResult: QueryTree<*>? = null // to be set + override val id = "cpg-0012" // TODO IDS + override val name = "Null Dereference" + override val shortDescription = "Detects null dereferences" + override val level = Rule.Level.Error + override val message = "Null dereference detected" + + override fun run(result: TranslationResult) { + queryResult = result.allExtended { const(it.base) eq const(null) } + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/PathTraversalViaUserInputConcatenationRule.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/PathTraversalViaUserInputConcatenationRule.kt new file mode 100644 index 0000000000..4ad4ec24f5 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/PathTraversalViaUserInputConcatenationRule.kt @@ -0,0 +1,80 @@ +/* + * 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.rules + +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.query.* + +class PathTraversalViaUserInputConcatenationRule : Rule { + override var queryResult: QueryTree<*>? = null + + override val id = "PTR-001" + override val name = "Path Traversal via User Input Concatenation" + override val cweId = "22" + override val shortDescription = + "User input is concatenated with file paths without proper validation" + override val mdShortDescription = null + override val level = Rule.Level.Warning + override val message = + "User input is concatenated with file paths without proper validation, leading to potential path traversal vulnerabilities." + override val mdMessage = null + override val messageArguments = listOf() + + private val concatFunctionsRegex = Regex("concat|append|app", RegexOption.IGNORE_CASE) + + override fun run(result: TranslationResult) { + queryResult = + result.allExtended( + sel = { Util.isUserInput(it) }, + mustSatisfy = { outer -> + dataFlow( // reaches concat + from = outer, + predicate = { inner -> + inner is CallExpression && + inner.name.localName.contains(concatFunctionsRegex) && + inner.arguments.any { Util.isPathLike(it) } || + inner is BinaryOperator && inner.operatorCode == "+" || + inner is BinaryOperator && inner.operatorCode == "+=" + }, + collectFailedPaths = false, + findAllPossiblePaths = true + ) and + not( // and doesn't reach validation + dataFlow( + from = outer, + predicate = { + it is CallExpression && Util.isValidationFunction(it) + }, + collectFailedPaths = false, + findAllPossiblePaths = true + ) + ) + } + ) + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/UseAfterFree.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/UseAfterFree.kt new file mode 100644 index 0000000000..a954fd0149 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/UseAfterFree.kt @@ -0,0 +1,55 @@ +/* + * 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.rules + +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference +import de.fraunhofer.aisec.cpg.query.* + +class UseAfterFree : Rule { + override var queryResult: QueryTree<*>? = null // to be set + override val id = "cpg-0015" + override val name = "Use After Free" + override val cweId = "416" + override val shortDescription = "Detects use of memory after it has been freed" + override val level = Rule.Level.Error + override val message = "Use after free detected" + + override fun run(result: TranslationResult) { + queryResult = + result.allExtended( + { it.name.localName == "free" }, + { outer -> + executionPath(outer) { inner -> + (outer.arguments[0] as? Reference)?.refersTo == // free argument + (inner as? Reference) + ?.refersTo // reference to free argument after the free + } + } + ) + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/Util.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/Util.kt new file mode 100644 index 0000000000..5311cc46a3 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/rules/Util.kt @@ -0,0 +1,58 @@ +/* + * 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.rules + +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression + +class Util { + companion object { + fun isPathLike(arg: Expression): Boolean { + return arg.type.name.localName.contains(Regex("str|char", RegexOption.IGNORE_CASE)) && + (arg.name.localName.contains( + Regex("path|file|dir|directory|url|uri", RegexOption.IGNORE_CASE) + ) || + arg.code?.contains( + Regex("path|file|dir|directory|url|uri", RegexOption.IGNORE_CASE) + ) ?: false || + arg.code?.contains('/') ?: false) + } + + fun isUserInput(arg: CallExpression): Boolean { + val userInputRegex = + Regex("user|input|param|arg|argument|request|query", RegexOption.IGNORE_CASE) + return arg.name.localName.contains(userInputRegex) || + arg.code?.contains(userInputRegex) ?: false + } + + fun isValidationFunction(arg: CallExpression): Boolean { + val validationFunctionRegex = + Regex("val|validate|check|san|sanitize|clean", RegexOption.IGNORE_CASE) + return arg.name.localName.contains(validationFunctionRegex) || + arg.code?.contains(validationFunctionRegex) ?: false + } + } +} diff --git a/cpg-console/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/RunPluginTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/query/SarifReporterTest.kt similarity index 59% rename from cpg-console/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/RunPluginTest.kt rename to cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/query/SarifReporterTest.kt index 00c48d31e9..c298b9dc6e 100644 --- a/cpg-console/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/RunPluginTest.kt +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/query/SarifReporterTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Fraunhofer AISEC. All rights reserved. + * 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. @@ -23,24 +23,8 @@ * \______/ \__| \______/ * */ -package de.fraunhofer.aisec.cpg.analysis +package de.fraunhofer.aisec.cpg.query -import de.fraunhofer.aisec.cpg.console.RunPlugin -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import org.jetbrains.kotlinx.ki.shell.Command -import org.jetbrains.kotlinx.ki.shell.configuration.ReplConfigurationBase - -class RunPluginTest { - object TestConfig : ReplConfigurationBase() - - @Test - fun testExecute() { - val plugin = RunPlugin().Load(TestConfig) - - val result = plugin.execute(":run") - assertTrue(result is Command.Result.RunSnippets) - assertEquals(4, result.snippetsToRun.toList().size) - } +class SarifReporterTest { + // TODO } diff --git a/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/CompilationDatabase.kt b/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/CompilationDatabase.kt index 0a84988d33..3ee80c0d52 100644 --- a/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/CompilationDatabase.kt +++ b/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/CompilationDatabase.kt @@ -69,18 +69,21 @@ class CompilationDatabase : Plugin { "val db = fromFile(File(\"$path\"))", // lets build and analyze "val config =\n" + - " TranslationConfiguration.builder()\n" + - " .useCompilationDatabase(db)\n" + - " .sourceLocations(db.sourceFiles)\n" + - " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage\")" + - " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.cxx.CPPLanguage\")" + - " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.java.JavaLanguage\")" + - " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.llvm.LLVMIRLanguage\")" + - " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.python.PythonLanguage\")" + - " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.golang.GoLanguage\")" + - " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.typescript.TypeScriptLanguage\")" + - " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.ruby.RubyLanguage\")" + - " .defaultPasses()\n" + + " TranslationConfiguration.builder()\n" + + " .useCompilationDatabase(db)\n" + + " .sourceLocations(db.sourceFiles)\n" + + " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage\")" + + " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.cxx.CPPLanguage\")" + + " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.java.JavaLanguage\")" + + " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.llvm.LLVMIRLanguage\")" + + " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.python.PythonLanguage\")" + + " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.golang.GoLanguage\")" + + " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.typescript.TypeScriptLanguage\")" + + " .optionalLanguage(\"de.fraunhofer.aisec.cpg.frontends.ruby.RubyLanguage\")" + + " .defaultPasses()\n" + + " .loadIncludes(true)\n" + + // " .useParallelFrontends(true)\n" + + // " .useParallelPasses(true)\n" + " .build()", "val analyzer = TranslationManager.builder().config(config).build()", "val result = analyzer.analyze().get()", diff --git a/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/CpgConsole.kt b/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/CpgConsole.kt index 43d41f3091..697522de16 100644 --- a/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/CpgConsole.kt +++ b/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/CpgConsole.kt @@ -26,7 +26,15 @@ package de.fraunhofer.aisec.cpg.console import de.fraunhofer.aisec.cpg.analysis.MultiLineToStringStyle +import de.fraunhofer.aisec.cpg.console.CpgConsole.configureREPL import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.query.Reporter +import de.fraunhofer.aisec.cpg.query.SarifReporter +import java.io.File +import java.nio.file.Path +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kotlin.script.experimental.api.ResultWithDiagnostics import kotlin.script.experimental.api.ScriptCompilationConfiguration import kotlin.script.experimental.api.ScriptEvaluationConfiguration import kotlin.script.experimental.api.compilerOptions @@ -34,19 +42,21 @@ import kotlin.script.experimental.jvm.baseClassLoader import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration import kotlin.script.experimental.jvm.dependenciesFromClassloader import kotlin.script.experimental.jvm.jvm +import kotlin.system.exitProcess +import org.jetbrains.kotlinx.ki.shell.Command import org.jetbrains.kotlinx.ki.shell.Plugin import org.jetbrains.kotlinx.ki.shell.Shell import org.jetbrains.kotlinx.ki.shell.configuration.CachedInstance import org.jetbrains.kotlinx.ki.shell.configuration.ReplConfiguration import org.jetbrains.kotlinx.ki.shell.configuration.ReplConfigurationBase +import org.jetbrains.kotlinx.ki.shell.wrappers.ResultWrapper +import picocli.CommandLine object CpgConsole { @JvmStatic fun main(args: Array) { - Node.TO_STRING_STYLE = MultiLineToStringStyle() - - val repl = configureREPL() - repl.doRun() + val exitCode = CommandLine(Cli()).execute(*args) // basically just runs [Cli.run] + exitProcess(exitCode) } private fun configuration(): ReplConfiguration { @@ -63,8 +73,11 @@ object CpgConsole { list += TranslatePlugin() list += Neo4jPlugin() list += ShowCodePlugin() - list += RunPlugin() list += CompilationDatabase() + list += RunPlugin() + list += LoadReporterPlugin() + list += RunRulePlugin() + list += ReportPlugin() return list.listIterator() } @@ -105,3 +118,220 @@ object CpgConsole { return repl } } + +// TODO: mby don't hardcode this here +enum class Language { + C, + CPP, + JAVA, + LLVMIR, + PYTHON, + GOLANG, + TYPESCRIPT, + RUBY, + ALL; + + fun toClassName(): String { + return when (this) { + C -> "de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage" + CPP -> "de.fraunhofer.aisec.cpg.frontends.cxx.CPPLanguage" + JAVA -> "de.fraunhofer.aisec.cpg.frontends.java.JavaLanguage" + LLVMIR -> "de.fraunhofer.aisec.cpg.frontends.llvm.LLVMIRLanguage" + PYTHON -> "de.fraunhofer.aisec.cpg.frontends.python.PythonLanguage" + GOLANG -> "de.fraunhofer.aisec.cpg.frontends.golang.GoLanguage" + TYPESCRIPT -> "de.fraunhofer.aisec.cpg.frontends.typescript.TypeScriptLanguage" + RUBY -> "de.fraunhofer.aisec.cpg.frontends.ruby.RubyLanguage" + ALL -> "" // optionalLanguage() doesnt crash + } + } +} + +@CommandLine.Command( + name = "cpg-analysis", + description = + [ + "Either specify no arguments to start an interactive shell or specify a compilation database and rules to run" + + " like a normal CLI tool." + ] +) +private class Cli : Runnable { + @CommandLine.Option( + names = ["-cdb", "--compilation-database"], + description = ["Path to the JSON compilation database."], + paramLabel = "FILE", + ) + var compilationDatabase: File? = null + + private class LanguageConverter : CommandLine.ITypeConverter { + override fun convert(value: String?): Language { + return try { + Language.valueOf(value!!.uppercase()) + } catch (_: Exception) { + throw CommandLine.ParameterException(CommandLine(this), "Invalid language:$value") + } // TODO: maybe ignore or just warn + } + } + + // TODO: support this + /* + @CommandLine.Option( + names = ["-l", "--languages"], + description = + [ + "Languages to analyze, any of {C, CPP, JAVA, LLVMIR, PYTHON, GOLANG, TYPESCRIPT, RUBY, ALL}. " + + "Case insensitive. Defaults to ALL." // TODO: whats actually available depends on build config + ], + split = ",", + converter = [LanguageConverter::class], + defaultValue = "ALL", + paramLabel = "language", + ) + val languages: List = emptyList() + */ + + @CommandLine.Option(names = ["-m", "--minify"], description = ["Minify the output."]) + var minify: Boolean = false + + @CommandLine.Option( + names = ["-o", "--output"], + description = + [ + "Path to write the output to. If unspecified, a default path is used. Used to determine the " + + "report type (currently only SARIF). The default is SARIF." + ], + paramLabel = "FILE", + ) + var outputPath: File = + Path.of( + "reports", + "report-${ + LocalDateTime.now().format( + DateTimeFormatter.ofPattern + ("yyyy-MM-dd-HH-mm-ss") + ) + }.sarif" + ) + .toFile() + + @CommandLine.Option( + names = ["-r", "--rules"], + description = ["Comma-separated list of rules to run. If unspecified, all rules are run."], + split = ",", + paramLabel = "rule", + ) + var rules: List = emptyList() + + // TODO: support this + /* + @CommandLine.Option( + names = ["--load-includes"], + description = ["Enable TranslationConfiguration option loadIncludes"], + ) + private var loadIncludes: Boolean = false + */ + + @SuppressWarnings("unused") // used by picocli + @CommandLine.Option( + names = ["-h", "--help"], + usageHelp = true, + description = ["display this help and exit."], + ) + var help: Boolean = false + + // TODO: add functionality + // -> Pass-related options from the neo4j app + + /** + * Runs the rules on the given compilation database and reports the results. + * + * The rules are currently built into the application and cannot be changed without modifying + * the source code. The output is a SARIF file because currently the only [Reporter] is the + * [SarifReporter]. The report's path is determined by the [Reporter.getDefaultPath] method of + * the respective [Reporter]. + */ + override fun run() { + Node.TO_STRING_STYLE = MultiLineToStringStyle() + + val repl = configureREPL() + + Runtime.getRuntime() + .addShutdownHook( + Thread { + // println("\nBye!") + repl.cleanUp() + } + ) + + val doInteractiveShell = compilationDatabase == null && rules.isEmpty() + if (doInteractiveShell) { + repl.doRun() + } else { + repl.initEngine() + lateinit var translateCompilationDatabasePlugin: Command + lateinit var loadReporterPlugin: Command + lateinit var runRulePlugin: Command + lateinit var reportPlugin: Command + val results = mutableListOf() + // find the commands we need + repl.commands.forEach { + when (it.short) { + "trdb" -> translateCompilationDatabasePlugin = it + "lr" -> loadReporterPlugin = it + "rr" -> runRulePlugin = it + "rp" -> reportPlugin = it + } + } + // load the specified reporter + results.add(loadReporterPlugin.execute(":loadReporter ${outputPath.extension}")) + // translate the compilation database + results.add( + translateCompilationDatabasePlugin.execute( + ":translateCompilationDatabase ${compilationDatabase?.absolutePath}" + ) + ) + // run the rules + for (rule in rules) results.add(runRulePlugin.execute(":runrule $rule")) + // create the report + results.add(reportPlugin.execute(":report ${outputPath.absolutePath} $minify")) + + // the following code is a copy of parts of the + // org.jetbrains.kotlinx.ki.shell.Shell.doRun() method + // of the kotlin-interactive-shell licensed under the Apache License 2.0. + // This is necessary because we want to run the shell headless which isn't directly + // supported. + // COPY START + var blankLines = 0 + fun evalSnippet(line: String) { + if (line.isBlank() && repl.incompleteLines.isNotEmpty()) { + if (blankLines == repl.settings.blankLinesAllowed - 1) { + repl.incompleteLines.clear() + println( + "You typed ${repl.settings.blankLinesAllowed} blank lines. Starting a new command." + ) + } else blankLines++ + } else { + val source = (repl.incompleteLines + line).joinToString(separator = "\n") + val time = System.nanoTime() + val result = repl.eval(source) + repl.evaluationTimeMillis = (System.nanoTime() - time) / 1_000_000 + when (result.getStatus()) { + ResultWrapper.Status.INCOMPLETE -> repl.incompleteLines.add(line) + ResultWrapper.Status.ERROR -> { + repl.incompleteLines.clear() + repl.handleError(result.result, result.isCompiled) + } + ResultWrapper.Status.SUCCESS -> { + repl.incompleteLines.clear() + repl.handleSuccess(result.result as ResultWithDiagnostics.Success<*>) + } + } + } + } + // COPY END + + results.forEach { + (it as Command.Result.RunSnippets).snippetsToRun.forEach(::evalSnippet) + } + } + } +} diff --git a/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/LoadReporterPlugin.kt b/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/LoadReporterPlugin.kt new file mode 100644 index 0000000000..dab5305047 --- /dev/null +++ b/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/LoadReporterPlugin.kt @@ -0,0 +1,70 @@ +/* + * 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. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.console + +import org.jetbrains.kotlinx.ki.shell.BaseCommand +import org.jetbrains.kotlinx.ki.shell.Command +import org.jetbrains.kotlinx.ki.shell.Plugin +import org.jetbrains.kotlinx.ki.shell.Shell +import org.jetbrains.kotlinx.ki.shell.configuration.ReplConfiguration + +class LoadReporterPlugin : Plugin { + inner class Load(conf: ReplConfiguration) : BaseCommand() { + override val name: String by conf.get(default = "loadReporter") + override val short: String by conf.get(default = "lr") + override val description: String = + "loads the specified reporter. Right now only a SARIF Reporter exists." + + override val params = "<{sarif}>" + + override fun execute(line: String): Command.Result { + val reportType = line.split(" ")[1] // [":lr", ""] + + return Command.Result.RunSnippets( + listOf( + "import de.fraunhofer.aisec.cpg.query.SarifReporter", + "var reporter = when (\"${reportType.lowercase()}\") {", + " \"sarif\" -> SarifReporter()", + " else -> SarifReporter()", // TODO: add more reporter types + "}", + "var rules = mutableListOf()", + ) + ) + } + } + + lateinit var repl: Shell + + override fun init(repl: Shell, config: ReplConfiguration) { + this.repl = repl + + repl.registerCommand(Load(config)) + } + + override fun cleanUp() { + // nothing to do + } +} diff --git a/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/ReportPlugin.kt b/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/ReportPlugin.kt new file mode 100644 index 0000000000..c23f1ee517 --- /dev/null +++ b/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/ReportPlugin.kt @@ -0,0 +1,85 @@ +/* + * 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. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.console + +import org.jetbrains.kotlinx.ki.shell.BaseCommand +import org.jetbrains.kotlinx.ki.shell.Command +import org.jetbrains.kotlinx.ki.shell.Plugin +import org.jetbrains.kotlinx.ki.shell.Shell +import org.jetbrains.kotlinx.ki.shell.configuration.ReplConfiguration + +class ReportPlugin : Plugin { + inner class Load(conf: ReplConfiguration) : BaseCommand() { + override val name: String by conf.get(default = "report") + override val short: String by conf.get(default = "rp") + override val description: String = + "creates a report for the current result of a :run command. " + + "Optionally provide a path to write the report to and a flag to minify the output." + + "requires a `reporter` and `rules` to be set, e.g. by running the :loadReporter and :runRule commands." + + override val params = "[path minify]" + + override fun execute(line: String): Command.Result { + // println(line.split(" ")) // [":report", "", ""] + var path = "" + var minify = "" + line.split(" ").let { + when (it.size) { + 3 -> { + path = it[1] + minify = it[2] + } + 2 -> { + if (it[1].toBoolean()) { + minify = it[1] + } else { + path = it[1] + } + } + else -> { + // nothing to do + } + } + } + var toRun = + "reporter.toFile(reporter.report(rules, $minify), kotlin.io.path.Path(\"$path\"))" + // println(toRun) + return Command.Result.RunSnippets(listOf(toRun)) + } + } + + lateinit var repl: Shell + + override fun init(repl: Shell, config: ReplConfiguration) { + this.repl = repl + + repl.registerCommand(Load(config)) + } + + override fun cleanUp() { + // nothing to do + } +} diff --git a/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/RunRulePlugin.kt b/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/RunRulePlugin.kt new file mode 100644 index 0000000000..799302c1d5 --- /dev/null +++ b/cpg-console/src/main/kotlin/de/fraunhofer/aisec/cpg/console/RunRulePlugin.kt @@ -0,0 +1,70 @@ +/* + * 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. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.console + +import org.jetbrains.kotlinx.ki.shell.BaseCommand +import org.jetbrains.kotlinx.ki.shell.Command +import org.jetbrains.kotlinx.ki.shell.Plugin +import org.jetbrains.kotlinx.ki.shell.Shell +import org.jetbrains.kotlinx.ki.shell.configuration.ReplConfiguration + +class RunRulePlugin : Plugin { + inner class Load(conf: ReplConfiguration) : BaseCommand() { + override val name: String by conf.get(default = "runRule") + override val short: String by conf.get(default = "rr") + override val description: String = + "runs a rule. Requires `result` to be set, e.g. by running the :translate " + "command." + + override val params = "" + + override fun execute(line: String): Command.Result { + val ruletoRun = line.split(" ")[1] // [":r", ""] + return Command.Result.RunSnippets( + listOf( + // import + "import de.fraunhofer.aisec.cpg.rules.$ruletoRun", + "import de.fraunhofer.aisec.cpg.query.SarifReporter", + // run it + "val rule = $ruletoRun()", + "rule.run(result)", + "rules.add(rule)", + ) + ) + } + } + + lateinit var repl: Shell + + override fun init(repl: Shell, config: ReplConfiguration) { + this.repl = repl + + repl.registerCommand(Load(config)) + } + + override fun cleanUp() { + // nothing to do + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f93c693ba2..34a9069d01 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,6 +43,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.26.0" } antlr-runtime = { module = "org.antlr:antlr4-runtime", version = "4.8-1" } # we cannot upgrade until ki-shell upgrades this! +sarif4k = { module = "io.github.detekt.sarif4k:sarif4k", version = "0.6.0"} # test junit-params = { module = "org.junit.jupiter:junit-jupiter-params", version = "5.10.0"}