Skip to content

Commit eafa496

Browse files
authored
Exporting QueryTree as SARIF result (#1967)
1 parent 3a7107a commit eafa496

File tree

17 files changed

+617
-117
lines changed

17 files changed

+617
-117
lines changed

codyze-compliance/src/integrationTest/kotlin/de/fraunhofer/aisec/codyze/compliance/CommandTest.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ class CommandIntegrationTest {
3838
"--project-dir src/integrationTest/resources/demo-app --components webapp --components auth"
3939
)
4040
assertEquals(
41-
"Message(arguments=null, id=null, markdown=This is a **finding**, properties=null, text=null)\n",
41+
"Message(arguments=null, id=null, markdown=null, properties=null, text=Query was successful)\n" +
42+
"Message(arguments=null, id=null, markdown=null, properties=null, text=Query was successful)\n",
4243
result.output,
4344
)
4445
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2025, Fraunhofer AISEC. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* $$$$$$\ $$$$$$$\ $$$$$$\
17+
* $$ __$$\ $$ __$$\ $$ __$$\
18+
* $$ / \__|$$ | $$ |$$ / \__|
19+
* $$ | $$$$$$$ |$$ |$$$$\
20+
* $$ | $$ ____/ $$ |\_$$ |
21+
* $$ | $$\ $$ | $$ | $$ |
22+
* \$$$$$ |$$ | \$$$$$ |
23+
* \______/ \__| \______/
24+
*
25+
*/
26+
package de.fraunhofer.aisec.codyze.compliance
27+
28+
import de.fraunhofer.aisec.codyze.AnalysisProject
29+
import de.fraunhofer.aisec.cpg.graph.*
30+
import kotlin.io.path.Path
31+
import kotlin.io.path.createTempFile
32+
import kotlin.test.Test
33+
import kotlin.test.assertNotNull
34+
import kotlin.test.assertTrue
35+
36+
class SarifTest {
37+
@Test
38+
fun testSarifFindings() {
39+
val project =
40+
AnalysisProject.from(
41+
projectDir = Path("src/integrationTest/resources/demo-app"),
42+
components = listOf("webapp"),
43+
)
44+
45+
val result = project.analyzeWithGoals()
46+
val tr = result.translationResult
47+
val webappMain = tr.namespaces["webapp.main"]
48+
assertNotNull(webappMain)
49+
50+
val tmpFile = createTempFile(prefix = "findings", suffix = ".sarif").toFile()
51+
result.writeSarifJson(tmpFile)
52+
53+
assertTrue(tmpFile.length() > 0)
54+
tmpFile.delete()
55+
}
56+
}

codyze-compliance/src/integrationTest/resources/demo-app/components/webapp/main.py

-4
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
Simulates the execution of a command line tool
3+
"""
4+
def execute(command, *args, stdin=None):
5+
pass
6+
7+
"""
8+
Simulates the retrieval of a secret from a server
9+
"""
10+
def get_secret_from_server() -> str:
11+
pass
12+
13+
14+
def encrypt():
15+
my_secret = get_secret_from_server()
16+
execute("encrypt",
17+
"--very-good",
18+
stdin=my_secret)
19+
del my_secret
20+
return
21+
22+
def decrypt():
23+
my_secret = get_secret_from_server()
24+
execute("decrypt",
25+
"--very-good",
26+
stdin=my_secret)
27+
del my_secret
28+
return

codyze-compliance/src/integrationTest/resources/demo-app/queries/good-encryption.query.kts

-16
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright (c) 2025, Fraunhofer AISEC. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* $$$$$$\ $$$$$$$\ $$$$$$\
17+
* $$ __$$\ $$ __$$\ $$ __$$\
18+
* $$ / \__|$$ | $$ |$$ / \__|
19+
* $$ | $$$$$$$ |$$ |$$$$\
20+
* $$ | $$ ____/ $$ |\_$$ |
21+
* $$ | $$\ $$ | $$ | $$ |
22+
* \$$$$$ |$$ | \$$$$$ |
23+
* \______/ \__| \______/
24+
*
25+
*/
26+
import de.fraunhofer.aisec.cpg.TranslationResult
27+
import de.fraunhofer.aisec.cpg.graph.*
28+
import de.fraunhofer.aisec.cpg.graph.edges.*
29+
import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression
30+
import de.fraunhofer.aisec.cpg.graph.statements.expressions.DeleteExpression
31+
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference
32+
import de.fraunhofer.aisec.cpg.query.QueryTree
33+
import de.fraunhofer.aisec.cpg.query.allExtended
34+
import de.fraunhofer.aisec.cpg.query.executionPath
35+
36+
fun statement1(tr: TranslationResult): QueryTree<Boolean> {
37+
val result =
38+
tr.allExtended<CallExpression>(
39+
sel = {
40+
it.name.toString() == "execute" &&
41+
it.arguments[0].evaluate() in listOf("encrypt", "decrypt")
42+
}
43+
) {
44+
val processInput = it.argumentEdges["stdin"]?.end
45+
if (processInput == null) {
46+
QueryTree(true)
47+
} else {
48+
executionPath(processInput) { to ->
49+
to is DeleteExpression &&
50+
to.operands.any {
51+
it is Reference && it.refersTo == (processInput as? Reference)?.refersTo
52+
}
53+
}
54+
}
55+
}
56+
57+
return result
58+
}

codyze-compliance/src/integrationTest/resources/demo-app/security-goals/goal1.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ components:
66
assumptions:
77
- Third party code is very good
88
objectives:
9-
- name: Good encryption
10-
description: Encryption used is very good
9+
- name: Proper handling of key material
10+
description: Sensitive material, such as keys are handled properly
1111
statements:
12-
- For each algorithm A, if A is used, then A must be a very good cryptographic algorithm
12+
- For each key K, if K is used in encryption or decryption, it must be deleted after use

codyze-compliance/src/main/kotlin/de/fraunhofer/aisec/codyze/compliance/Command.kt

+14-47
Original file line numberDiff line numberDiff line change
@@ -40,58 +40,23 @@ class ComplianceCommand : CliktCommand() {
4040
* all commands.
4141
*/
4242
abstract class ProjectCommand : CliktCommand() {
43-
private val projectOptions by ProjectOptions()
44-
private val translationOptions by TranslationOptions()
45-
46-
/** Loads the security goals from the project. */
47-
fun loadSecurityGoals(): List<SecurityGoal> {
48-
return loadSecurityGoals(projectOptions.directory.resolve("security-goals"))
49-
}
50-
51-
/**
52-
* This method is called by the `run` method to perform the actual analysis. It is separated to
53-
* allow for easier access from overriding applications.
54-
*/
55-
protected fun analyze(): AnalysisResult {
56-
// Load the security goals from the project
57-
val goals = loadSecurityGoals(projectOptions.directory.resolve("security-goals"))
58-
59-
// Analyze the project
60-
val project = AnalysisProject.fromOptions(projectOptions, translationOptions)
61-
val result = project.analyze()
62-
val tr = result.translationResult
63-
64-
// Connect the security goals to the translation result for now. Later we will add them to
65-
// individual concepts
66-
for (goal in goals) {
67-
goal.underlyingNode = tr
68-
69-
// Load and execute queries associated to the goals
70-
for (objective in goal.objectives) {
71-
objective.underlyingNode = tr
72-
73-
val scriptFile =
74-
projectOptions.directory
75-
.resolve("queries")
76-
.resolve(
77-
"${objective.name.localName.lowercase().replace(" ", "-")}.query.kts"
78-
)
79-
for (stmt in objective.statements.withIndex()) {
80-
tr.evalQuery(scriptFile.toFile(), "statement${stmt.index + 1}")
81-
}
82-
}
83-
}
84-
85-
return result
86-
}
43+
protected val projectOptions by ProjectOptions()
44+
protected val translationOptions by TranslationOptions()
8745
}
8846

8947
/** The `scan` command. This will scan the project for compliance violations in the future. */
9048
open class ScanCommand : ProjectCommand() {
9149
override fun run() {
92-
val result = analyze()
50+
val project =
51+
AnalysisProject.fromOptions(projectOptions, translationOptions) {
52+
// just to show that we can use a config build here
53+
it
54+
}
55+
val result = project.analyzeWithGoals()
9356

94-
result.run.results?.forEach { echo(it.message) }
57+
result.sarif.runs.forEach { run ->
58+
run.results?.forEach { result -> echo(result.message.toString()) }
59+
}
9560
}
9661
}
9762

@@ -104,7 +69,9 @@ open class ScanCommand : ProjectCommand() {
10469
*/
10570
class ListSecurityGoals : ProjectCommand() {
10671
override fun run() {
107-
val goals = loadSecurityGoals()
72+
val project = AnalysisProject.fromOptions(projectOptions, translationOptions)
73+
val goals = project.loadSecurityGoals()
74+
10875
// Print the name of each security goal
10976
goals.forEach { echo(it.name.localName) }
11077
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright (c) 2025, Fraunhofer AISEC. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* $$$$$$\ $$$$$$$\ $$$$$$\
17+
* $$ __$$\ $$ __$$\ $$ __$$\
18+
* $$ / \__|$$ | $$ |$$ / \__|
19+
* $$ | $$$$$$$ |$$ |$$$$\
20+
* $$ | $$ ____/ $$ |\_$$ |
21+
* $$ | $$\ $$ | $$ | $$ |
22+
* \$$$$$ |$$ | \$$$$$ |
23+
* \______/ \__| \______/
24+
*
25+
*/
26+
package de.fraunhofer.aisec.codyze.compliance
27+
28+
import de.fraunhofer.aisec.codyze.*
29+
import de.fraunhofer.aisec.cpg.TranslationResult
30+
import io.github.detekt.sarif4k.MultiformatMessageString
31+
import io.github.detekt.sarif4k.ReportingDescriptor
32+
import io.github.detekt.sarif4k.Result
33+
34+
/** Loads the security goals from the project directory. */
35+
fun AnalysisProject.loadSecurityGoals(): List<SecurityGoal> {
36+
return securityGoalsFolder?.let { loadSecurityGoals(it) } ?: listOf()
37+
}
38+
39+
/**
40+
* Extends the regular [AnalysisProject.analyze] method with the ability to load security goals and
41+
* execute queries based on them.
42+
*/
43+
fun AnalysisProject.analyzeWithGoals(): AnalysisResult {
44+
return this.analyze(postProcess = ::executeSecurityGoalsQueries)
45+
}
46+
47+
/**
48+
* Executes the security goals queries and returns the security goals as SARIF rules and the query
49+
* results as SARIF results.
50+
*/
51+
fun AnalysisProject.executeSecurityGoalsQueries(
52+
tr: TranslationResult
53+
): Pair<List<ReportingDescriptor>, List<Result>> {
54+
val rules = mutableListOf<ReportingDescriptor>()
55+
val results = mutableListOf<Result>()
56+
val goals = loadSecurityGoals()
57+
58+
// Connect the security goals to the translation result for now. Later we will add them
59+
// to individual concepts
60+
for (goal in goals) {
61+
goal.underlyingNode = tr
62+
63+
// Load and execute queries associated to the goals
64+
for (objective in goal.objectives) {
65+
val objectiveID = objective.name.localName.lowercase().replace(" ", "-")
66+
objective.underlyingNode = tr
67+
68+
projectDir?.let {
69+
val scriptFile = it.resolve("queries").resolve("${objectiveID}.query.kts")
70+
for (stmt in objective.statements.withIndex()) {
71+
val idx1 = stmt.index + 1
72+
val statementID = "statement${idx1}"
73+
val rule =
74+
ReportingDescriptor(
75+
id = "${objectiveID}-${statementID}",
76+
name = "${objective.name.localName}: Statement $idx1",
77+
shortDescription = MultiformatMessageString(text = stmt.value),
78+
)
79+
val queryResult = tr.evalQuery(scriptFile.toFile(), statementID, rule.id)
80+
results += queryResult.sarif
81+
82+
rules += rule
83+
}
84+
}
85+
}
86+
}
87+
88+
return Pair(rules, results)
89+
}

codyze-compliance/src/main/kotlin/de/fraunhofer/aisec/codyze/compliance/SecurityGoal.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ fun loadSecurityGoal(stream: InputStream, result: TranslationResult? = null): Se
132132
}
133133

134134
/**
135-
* This function returns a [Yaml] instance that is configured to use the given [result] to resolve
136-
* components.
135+
* This function returns a [com.charleskorn.kaml.Yaml] instance that is configured to use the given
136+
* [result] to resolve components.
137137
*/
138138
private fun yaml(result: TranslationResult?): Yaml {
139139
val module = SerializersModule { contextual(Component::class, ComponentSerializer(result)) }

0 commit comments

Comments
 (0)