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

Basic implementation of compliance scan command #1940

Merged
merged 19 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 17 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
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"))
oxisto marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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"))
oxisto marked this conversation as resolved.
Show resolved Hide resolved

// 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() {
oxisto marked this conversation as resolved.
Show resolved Hide resolved
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())

Check warning on line 113 in codyze-compliance/src/main/kotlin/de/fraunhofer/aisec/codyze/compliance/Command.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L113 was not covered by tests
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
Loading