Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Declarative cli tool to make use of the query capabilities #1626

Draft
wants to merge 44 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a440e70
Reporter interface
MariusAlbrecht Apr 20, 2024
4b6a75f
sarif specific Reporter
MariusAlbrecht Apr 20, 2024
f2195d3
io.github.detekt.sarif4k dependency for sarif building
MariusAlbrecht Apr 20, 2024
5f05f16
Rule Interface
MariusAlbrecht Apr 20, 2024
6221fff
move Reporter, Rule and SarifReporter
MariusAlbrecht Jun 11, 2024
6d197b8
move Reporter, Rule and SarifReporter
MariusAlbrecht Jun 11, 2024
a559a3d
add sarif4k dependency
MariusAlbrecht Jun 11, 2024
8962c6a
move sarif4k dependency
MariusAlbrecht Jun 11, 2024
f3c668c
RuleRunner added
MariusAlbrecht Jun 11, 2024
5414de8
Merge branch 'refs/heads/main' into queries
MariusAlbrecht Jun 11, 2024
03f31b2
Merge remote-tracking branch 'refs/remotes/upstream/main' into queries
MariusAlbrecht Jun 18, 2024
d1f9e99
add cwe to rule interface
MariusAlbrecht Jun 18, 2024
1fdcaca
add cwe to SarifReporter
MariusAlbrecht Jun 18, 2024
59eae97
print location of report in Reporter
MariusAlbrecht Jun 18, 2024
b5db0de
add proper children to Query.(sizeof | max | min)
MariusAlbrecht Jul 21, 2024
d9ec7ee
cleanup
MariusAlbrecht Jul 21, 2024
6fd818f
make RuleRunner a CLI app
MariusAlbrecht Jul 21, 2024
ea706a1
adjust build.gradle.kts to build the RuleRunner CLI app
MariusAlbrecht Jul 21, 2024
7341c01
Merge branch 'refs/heads/main' into queries
MariusAlbrecht Jul 21, 2024
a17d5a5
move Reporter, Rule and SarifReporter
MariusAlbrecht Jun 11, 2024
a113b4c
add sarif4k dependency
MariusAlbrecht Jun 11, 2024
54cf9da
RuleRunner added
MariusAlbrecht Jun 11, 2024
0ab28e4
add cwe to rule interface
MariusAlbrecht Jun 18, 2024
8ecd289
add cwe to SarifReporter
MariusAlbrecht Jun 18, 2024
66ee91e
print location of report in Reporter
MariusAlbrecht Jun 18, 2024
67c97db
cleanup
MariusAlbrecht Jul 21, 2024
233d013
make RuleRunner a CLI app
MariusAlbrecht Jul 21, 2024
3fd6e85
adjust build.gradle.kts to build the RuleRunner CLI app
MariusAlbrecht Jul 21, 2024
efcefb4
add languages to build.gradle.kts
MariusAlbrecht Jun 11, 2024
27fb6e6
add loadIncludes option
MariusAlbrecht Jul 22, 2024
10e4f35
dont include QueryTree stuff
MariusAlbrecht Jul 22, 2024
c7708f1
add sarif4k to libs.versions.toml
MariusAlbrecht Jul 22, 2024
283a9c3
formatting
MariusAlbrecht Jul 22, 2024
0d80c05
fix references to code not included
MariusAlbrecht Jul 22, 2024
8035735
formatting again
MariusAlbrecht Jul 22, 2024
7d31614
fix missing test dependency (?)
MariusAlbrecht Jul 22, 2024
cf3f6f9
Merge branch 'refs/heads/main' into queries
MariusAlbrecht Aug 14, 2024
b35538b
add some Rules and helper functions
MariusAlbrecht Oct 6, 2024
77862f8
add option to run console as a non-interactive cli tool and add Plugi…
MariusAlbrecht Oct 6, 2024
a09f0ec
add Plugin to run rules and small formatting changes
MariusAlbrecht Oct 6, 2024
061c31c
add cli arguments argument in Reporter to report `invocations`
MariusAlbrecht Oct 6, 2024
8806539
properly add children to some QueryTree functions so that they result…
MariusAlbrecht Oct 6, 2024
8680f49
remove duplicate sarif4k libs entry
MariusAlbrecht Oct 6, 2024
b2bca64
fiX formatting errors in CpgConsole
MariusAlbrecht Oct 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions cpg-analysis/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
*
*/
plugins {
id("cpg.application-conventions")
id("cpg.library-conventions")
}

Expand All @@ -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))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -120,7 +117,7 @@ inline fun <reified T> Node.exists(
*/
fun sizeof(n: Node?, eval: ValueEvaluator = SizeEvaluator()): QueryTree<Int> {
// 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)")
}

/**
Expand All @@ -134,7 +131,7 @@ fun min(n: Node?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree<Numbe
return QueryTree(evalRes, mutableListOf(QueryTree(n)), "min($n)")
}
// Extend this when we have other evaluators.
return QueryTree((evalRes as? NumberSet)?.min() ?: -1, mutableListOf(), "min($n)")
return QueryTree((evalRes as? NumberSet)?.min() ?: -1, mutableListOf(QueryTree(n)), "min($n)")
}

/**
Expand All @@ -155,7 +152,7 @@ fun min(n: List<Node>?, 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)")
}

/**
Expand All @@ -176,7 +173,7 @@ fun max(n: List<Node>?, 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)")
}

/**
Expand All @@ -190,7 +187,7 @@ fun max(n: Node?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree<Numbe
return QueryTree(evalRes, mutableListOf(QueryTree(n)))
}
// Extend this when we have other evaluators.
return QueryTree((evalRes as? NumberSet)?.max() ?: -1, mutableListOf(), "max($n)")
return QueryTree((evalRes as? NumberSet)?.max() ?: -1, mutableListOf(QueryTree(n)), "max($n)")
}

/** Checks if a data flow is possible between the nodes [from] as a source and [to] as sink. */
Expand Down Expand Up @@ -250,8 +247,8 @@ fun executionPath(from: Node, to: Node): QueryTree<Boolean> {
*/
fun executionPath(from: Node, predicate: (Node) -> Boolean): QueryTree<Boolean> {
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(),
Expand Down Expand Up @@ -320,91 +317,3 @@ val Expression.size: QueryTree<Int>
get() {
return sizeof(this)
}

/**
* The minimal integer value of this expression. It uses the default argument for `eval` of [min]
*/
val Expression.min: QueryTree<Number>
get() {
return min(this)
}

/**
* The maximal integer value of this expression. It uses the default argument for `eval` of [max]
*/
val Expression.max: QueryTree<Number>
get() {
return max(this)
}

/** Calls [ValueEvaluator.evaluate] for this expression, thus trying to resolve a constant value. */
val Expression.value: QueryTree<Any?>
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<Int>?
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<List<Node>>): QueryTree<Boolean> {
return when (from) {
is CallExpression -> {
val prevEdges =
from.prevDFG
.fold(mutableListOf<Node>()) { 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())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,50 @@ open class QueryTree<T>(
}
throw QueryException("Cannot compare objects of type ${this.value} and $other")
}

operator fun plus(increment: Int): QueryTree<Number> {
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<Number> {
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<Number> {
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<Number> {
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. */
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Rule>,
minify: Boolean = false,
arguments: List<String> = 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-<yyyy-MM-dd-HH-mm-ss>.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")
}
}
Loading
Loading