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

Fixes for parsing OpenStack #1985

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class CommandIntegrationTest {
"--project-dir src/integrationTest/resources/demo-app --components webapp --components auth"
)
assertEquals(
"Message(arguments=null, id=null, markdown=This is a **finding**, properties=null, text=null)\n",
"Message(arguments=null, id=null, markdown=null, properties=null, text=Query was successful)\n" +
"Message(arguments=null, id=null, markdown=null, properties=null, text=Query was successful)\n",
result.output,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2025, 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.codyze.compliance

import de.fraunhofer.aisec.codyze.AnalysisProject
import de.fraunhofer.aisec.cpg.graph.*
import kotlin.io.path.Path
import kotlin.io.path.createTempFile
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class SarifTest {
@Test
fun testSarifFindings() {
val project =
AnalysisProject.from(
projectDir = Path("src/integrationTest/resources/demo-app"),
components = listOf("webapp"),
)

val result = project.analyzeWithGoals()
val tr = result.translationResult
val webappMain = tr.namespaces["webapp.main"]
assertNotNull(webappMain)

val tmpFile = createTempFile(prefix = "findings", suffix = ".sarif").toFile()
result.writeSarifJson(tmpFile)

assertTrue(tmpFile.length() > 0)
tmpFile.delete()
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Simulates the execution of a command line tool
"""
def execute(command, *args, stdin=None):
pass

"""
Simulates the retrieval of a secret from a server
"""
def get_secret_from_server() -> str:
pass


def encrypt():
my_secret = get_secret_from_server()
execute("encrypt",
"--very-good",
stdin=my_secret)
del my_secret
return

def decrypt():
my_secret = get_secret_from_server()
execute("decrypt",
"--very-good",
stdin=my_secret)
del my_secret
return

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025, 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.
*
* $$$$$$\ $$$$$$$\ $$$$$$\
* $$ __$$\ $$ __$$\ $$ __$$\
* $$ / \__|$$ | $$ |$$ / \__|
* $$ | $$$$$$$ |$$ |$$$$\
* $$ | $$ ____/ $$ |\_$$ |
* $$ | $$\ $$ | $$ | $$ |
* \$$$$$ |$$ | \$$$$$ |
* \______/ \__| \______/
*
*/
import de.fraunhofer.aisec.cpg.TranslationResult
import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.edges.*
import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.DeleteExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference
import de.fraunhofer.aisec.cpg.query.QueryTree
import de.fraunhofer.aisec.cpg.query.allExtended
import de.fraunhofer.aisec.cpg.query.executionPath

fun statement1(tr: TranslationResult): QueryTree<Boolean> {
val result =
tr.allExtended<CallExpression>(
sel = {
it.name.toString() == "execute" &&
it.arguments[0].evaluate() in listOf("encrypt", "decrypt")
}
) {
val processInput = it.argumentEdges["stdin"]?.end
if (processInput == null) {
QueryTree(true)
} else {
executionPath(processInput) { to ->
to is DeleteExpression &&
to.operands.any {
it is Reference && it.refersTo == (processInput as? Reference)?.refersTo
}
}
}
}

return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ components:
assumptions:
- Third party code is very good
objectives:
- name: Good encryption
description: Encryption used is very good
- name: Proper handling of key material
description: Sensitive material, such as keys are handled properly
statements:
- For each algorithm A, if A is used, then A must be a very good cryptographic algorithm
- For each key K, if K is used in encryption or decryption, it must be deleted after use
Original file line number Diff line number Diff line change
Expand Up @@ -40,58 +40,23 @@ class ComplianceCommand : CliktCommand() {
* all commands.
*/
abstract class ProjectCommand : CliktCommand() {
private val projectOptions by ProjectOptions()
private val translationOptions by TranslationOptions()

/** Loads the security goals from the project. */
fun loadSecurityGoals(): List<SecurityGoal> {
return loadSecurityGoals(projectOptions.directory.resolve("security-goals"))
}

/**
* This method is called by the `run` method to perform the actual analysis. It is separated to
* allow for easier access from overriding applications.
*/
protected fun analyze(): AnalysisResult {
// Load the security goals from the project
val goals = loadSecurityGoals(projectOptions.directory.resolve("security-goals"))

// Analyze the project
val project = AnalysisProject.fromOptions(projectOptions, translationOptions)
val result = project.analyze()
val tr = result.translationResult

// Connect the security goals to the translation result for now. Later we will add them to
// individual concepts
for (goal in goals) {
goal.underlyingNode = tr

// Load and execute queries associated to the goals
for (objective in goal.objectives) {
objective.underlyingNode = tr

val scriptFile =
projectOptions.directory
.resolve("queries")
.resolve(
"${objective.name.localName.lowercase().replace(" ", "-")}.query.kts"
)
for (stmt in objective.statements.withIndex()) {
tr.evalQuery(scriptFile.toFile(), "statement${stmt.index + 1}")
}
}
}

return result
}
protected val projectOptions by ProjectOptions()
protected val translationOptions by TranslationOptions()
}

/** The `scan` command. This will scan the project for compliance violations in the future. */
open class ScanCommand : ProjectCommand() {
override fun run() {
val result = analyze()
val project =
AnalysisProject.fromOptions(projectOptions, translationOptions) {
// just to show that we can use a config build here
it
}
val result = project.analyzeWithGoals()

result.run.results?.forEach { echo(it.message) }
result.sarif.runs.forEach { run ->
run.results?.forEach { result -> echo(result.message.toString()) }
}
}
}

Expand All @@ -104,7 +69,9 @@ open class ScanCommand : ProjectCommand() {
*/
class ListSecurityGoals : ProjectCommand() {
override fun run() {
val goals = loadSecurityGoals()
val project = AnalysisProject.fromOptions(projectOptions, translationOptions)
val goals = project.loadSecurityGoals()

// Print the name of each security goal
goals.forEach { echo(it.name.localName) }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2025, 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.codyze.compliance

import de.fraunhofer.aisec.codyze.*
import de.fraunhofer.aisec.cpg.TranslationResult
import io.github.detekt.sarif4k.MultiformatMessageString
import io.github.detekt.sarif4k.ReportingDescriptor
import io.github.detekt.sarif4k.Result

/** Loads the security goals from the project directory. */
fun AnalysisProject.loadSecurityGoals(): List<SecurityGoal> {
return securityGoalsFolder?.let { loadSecurityGoals(it) } ?: listOf()
}

/**
* Extends the regular [AnalysisProject.analyze] method with the ability to load security goals and
* execute queries based on them.
*/
fun AnalysisProject.analyzeWithGoals(): AnalysisResult {
return this.analyze(postProcess = ::executeSecurityGoalsQueries)
}

/**
* Executes the security goals queries and returns the security goals as SARIF rules and the query
* results as SARIF results.
*/
fun AnalysisProject.executeSecurityGoalsQueries(
tr: TranslationResult
): Pair<List<ReportingDescriptor>, List<Result>> {
val rules = mutableListOf<ReportingDescriptor>()
val results = mutableListOf<Result>()
val goals = loadSecurityGoals()

// Connect the security goals to the translation result for now. Later we will add them
// to individual concepts
for (goal in goals) {
goal.underlyingNode = tr

// Load and execute queries associated to the goals
for (objective in goal.objectives) {
val objectiveID = objective.name.localName.lowercase().replace(" ", "-")
objective.underlyingNode = tr

projectDir?.let {
val scriptFile = it.resolve("queries").resolve("${objectiveID}.query.kts")
for (stmt in objective.statements.withIndex()) {
val idx1 = stmt.index + 1
val statementID = "statement${idx1}"
val rule =
ReportingDescriptor(
id = "${objectiveID}-${statementID}",
name = "${objective.name.localName}: Statement $idx1",
shortDescription = MultiformatMessageString(text = stmt.value),
)
val queryResult = tr.evalQuery(scriptFile.toFile(), statementID, rule.id)
results += queryResult.sarif

rules += rule
}
}
}
}

return Pair(rules, results)
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ fun loadSecurityGoal(stream: InputStream, result: TranslationResult? = null): Se
}

/**
* This function returns a [Yaml] instance that is configured to use the given [result] to resolve
* components.
* This function returns a [com.charleskorn.kaml.Yaml] instance that is configured to use the given
* [result] to resolve components.
*/
private fun yaml(result: TranslationResult?): Yaml {
val module = SerializersModule { contextual(Component::class, ComponentSerializer(result)) }
Expand Down
Loading
Loading