Skip to content

Commit

Permalink
make RuleRunner a CLI app
Browse files Browse the repository at this point in the history
  • Loading branch information
MariusAlbrecht committed Jul 21, 2024
1 parent d9ec7ee commit 6fd818f
Showing 1 changed file with 165 additions and 46 deletions.
211 changes: 165 additions & 46 deletions cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/RuleRunner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Rule>,
private var languages: List<Language>,
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<Language?> {
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<Language> = 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<String>) {
val exitCode = CommandLine(Cli()).execute(*args)
exitProcess(exitCode)
}

0 comments on commit 6fd818f

Please sign in to comment.