Skip to content

Commit

Permalink
Basic implementation of compliance scan command (#1940)
Browse files Browse the repository at this point in the history
  • Loading branch information
oxisto authored Jan 22, 2025
1 parent 8425ce0 commit 0110554
Show file tree
Hide file tree
Showing 28 changed files with 759 additions and 97 deletions.
12 changes: 12 additions & 0 deletions buildSrc/src/main/kotlin/codyze.module-conventions.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import org.gradle.accessors.dm.LibrariesForLibs

plugins {
id("cpg.common-conventions")
id("cpg.frontend-conventions")
}

val libs = the<LibrariesForLibs>() // necessary to be able to use the version catalog in buildSrc
dependencies {
api(project(":codyze-core"))
api(libs.clikt)
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ val headerWithHashes = """#

spotless {
kotlin {
targetExclude("**/*.query.kts")
ktfmt().kotlinlangStyle()
licenseHeader(headerWithStars).yearSeparator(" - ")
}
Expand Down
12 changes: 9 additions & 3 deletions codyze-compliance/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*
*/
plugins {
id("cpg.frontend-conventions")
id("codyze.module-conventions")
}

publishing {
Expand All @@ -40,7 +40,13 @@ publishing {
}

dependencies {
implementation(projects.cpgCore)
implementation(libs.clikt)
implementation(libs.kaml)

// We depend on the Python frontend for the integration tests, but the frontend is only available if enabled.
// If it's not available, the integration tests fail (which is ok). But if we would directly reference the
// project here, the build system would fail any task since it will not find a non-enabled project.
findProject(":cpg-language-python")?.also {
integrationTestImplementation(it)
}
integrationTestImplementation(libs.clikt)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 com.github.ajalt.clikt.testing.test
import kotlin.test.Test
import kotlin.test.assertEquals

class CommandIntegrationTest {
@Test
fun testScanCommand() {
val command = ScanCommand()
val result =
command.test(
"--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",
result.output,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
print("Hello World")

def encrypt():
return very_good_encryption()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
print("Hello World")

def encrypt():
return very_good_encryption()
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import de.fraunhofer.aisec.cpg.TranslationResult
import de.fraunhofer.aisec.cpg.graph.calls
import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration
import de.fraunhofer.aisec.cpg.query.QueryTree
import de.fraunhofer.aisec.cpg.query.allExtended

fun statement1(tr: TranslationResult): QueryTree<Boolean> {
val result = tr.allExtended<FunctionDeclaration>(sel = {
it.name.localName.contains("encrypt") && !it.isInferred
}) {
QueryTree(it.calls.any {
it.name.contains("very_good")
})
}
return result
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Goal1
description: Make it very secure
components:
- auth
- webserver
assumptions:
- Third party code is very good
objectives:
- name: Good encryption
description: Encryption used is very good
statements:
- For each algorithm A, if A is used, then A must be a very good cryptographic algorithm
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* 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 com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import de.fraunhofer.aisec.codyze.*

/** The main `compliance` command. */
class ComplianceCommand : CliktCommand() {
override fun run() {}
}

/**
* A command that operates on a project. This class provides the common options and functions for
* 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
}
}

/** The `scan` command. This will scan the project for compliance violations in the future. */
open class ScanCommand : ProjectCommand() {
override fun run() {
val result = analyze()

result.run.results?.forEach { echo(it.message) }
}
}

/**
* The `list-security-goals` command. This will list the names of all security goals in the
* specified project.
*
* This command assumes that the project contains a folder named `security-goals` that contains YAML
* files with the security goals.
*/
class ListSecurityGoals : ProjectCommand() {
override fun run() {
val goals = loadSecurityGoals()
// Print the name of each security goal
goals.forEach { echo(it.name.localName) }
}
}

var Command = ComplianceCommand().subcommands(ScanCommand(), ListSecurityGoals())
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* \______/ \__| \______/
*
*/
package de.fraunhofer.aisec.cpg.codyze.compliance
package de.fraunhofer.aisec.codyze.compliance

import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.decodeFromStream
Expand Down Expand Up @@ -96,11 +96,8 @@ class ComponentSerializer(val result: TranslationResult?) : KSerializer<Componen
// Use the context to find the component by name
val componentName = decoder.decodeString()

return if (result != null) {
result.components.first { it.name.localName == componentName }
} else {
Component().also { it.name = Name(componentName) }
}
return result?.components?.first { it.name.localName == componentName }
?: Component().also { it.name = Name(componentName) }
}
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* \______/ \__| \______/
*
*/
package de.fraunhofer.aisec.cpg.codyze.compliance
package de.fraunhofer.aisec.codyze.compliance

import com.github.ajalt.clikt.testing.test
import kotlin.test.*
Expand All @@ -44,14 +44,4 @@ class CommandTest {
assertEquals(0, result.statusCode)
assertEquals("Goal1\n", result.stdout)
}

@Test
fun testScanCommand() {
val command = ScanCommand()
val ex = assertFails {
val result = command.test("--project-dir src/test/resources/")
assertEquals(0, result.statusCode)
}
assertIs<NotImplementedError>(ex)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* \______/ \__| \______/
*
*/
package de.fraunhofer.aisec.cpg.codyze.compliance
package de.fraunhofer.aisec.codyze.compliance

import de.fraunhofer.aisec.cpg.ScopeManager
import de.fraunhofer.aisec.cpg.TranslationConfiguration
Expand Down
14 changes: 14 additions & 0 deletions codyze-compliance/src/test/resources/log4j2.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Configuration status="WARN">
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss,SSS} %-5p %C{1} %m%n"/>
<ThresholdFilter level="DEBUG"/>
</Console>
</Appenders>
<Loggers>
<Logger level="DEBUG" name="de.fraunhofer.aisec"/>
<Root level="DEBUG">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>
</Configuration>
Loading

0 comments on commit 0110554

Please sign in to comment.