From 6fd818f4fcb4d9176cf0fe1b8329e1bbbe1172de Mon Sep 17 00:00:00 2001 From: Marius Albrecht Date: Sun, 21 Jul 2024 22:27:51 +0200 Subject: [PATCH] make RuleRunner a CLI app --- .../fraunhofer/aisec/cpg/query/RuleRunner.kt | 211 ++++++++++++++---- 1 file changed, 165 insertions(+), 46 deletions(-) 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 index 13522f62c9..474972bf15 100644 --- 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 @@ -28,78 +28,197 @@ 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.rules.BufferOverflowMemCpySizeLargerThanDest -import de.fraunhofer.aisec.cpg.rules.BufferOverreadMemCpySrcSmallerThanSize +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 codebase and reports the results + * A class that runs a set of rules on a given [CompilationDatabase] and reports the results * - * @param location the path to the codebase as used by [TranslationConfiguration.sourceLocations] - * @param language the language of the codebase, \in {"C", "CPP", "Go", "Java"} * @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( - language: String, private val rules: List, + private var languages: List, private val reporter: Reporter, - vararg locations: File + compilationDatabase: CompilationDatabase, ) { - private val config: TranslationConfiguration = TranslationConfiguration.builder() - .sourceLocations(locations.toList()) - // .optionalLanguage( - // when (language) { - // "C" -> "de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage" - // "CPP" -> "de.fraunhofer.aisec.cpg.frontends.cxx.CPPLanguage" - // "Go" -> "de.fraunhofer.aisec.cpg.frontends.golang.GoLanguage" - // "Java" -> "de.fraunhofer.aisec.cpg.frontends.java.JavaLanguage" - // else -> { - // println("Unsupported/No language, defaulting to C to avoid exception") - // "de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage" // don't throw - // exception - // } - // } - // ) - .optionalLanguage("de.fraunhofer.aisec.cpg.frontends.cxx.CPPLanguage") - .optionalLanguage("de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage") - .useParallelPasses(true) - .useParallelFrontends(true) + .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 + } .defaultPasses() .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) } } - fun report() { - reporter.toFile(reporter.report(rules), reporter.getDefaultPath()) + /** + * 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 + + @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( + AdditionIntegerOverflow(), + ArrayOverRead(), + ArrayUnderRead(), + AssignmentIntegerOverflow(), + BufferOverflowMemCpySizeLargerThanDest(), + BufferOverflowMemCpySrcLargerThanDest(), + BufferOverreadMemcpy(), + DoubleFree(), + IncrementIntegerOverflow(), + UseAfterFree(), + ), + languages = languages, + reporter = SarifReporter(), + compilationDatabase = CompilationDatabase.fromFile(compilationDatabase) + ) + runner.runRules() + if (outputPath != null) runner.report(minify = minify, path = outputPath!!.toPath()) + else runner.report(minify = minify) } } -fun main() { - val path = - // "/home/layla/code/cpg/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/programs/" - "/home/layla/code/cpg/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/programs/CWE121_Stack_Based_Buffer_Overflow__CWE805_struct_alloca_memcpy_31.c" - val runner = - RuleRunner( - language = "C", - rules = - listOf( - BufferOverflowMemCpySizeLargerThanDest(), - BufferOverflowMemCpySizeLargerThanDest(), - BufferOverreadMemCpySrcSmallerThanSize() - ), - reporter = SarifReporter(), - /* locations = */ File(path) - ) - runner.runRules() - runner.report() +fun main(args: Array) { + val exitCode = CommandLine(Cli()).execute(*args) + exitProcess(exitCode) }