From 7dcb7b10d777b3d4db87fd218e5b488db1df9b57 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Wed, 26 Aug 2026 19:51:29 +0200 Subject: [PATCH 1/6] feat(go-models): migrate built-in Java models --- .../kotlin => opentaint/kotlinx/coroutines}/Builders.java | 7 +++---- .../kotlinx/coroutines}/BuildersBuilders.java | 6 +++--- .../kotlinx/coroutines}/BuildersBuildersCommon.java | 6 +++--- .../stdlib => opentaint/java/lang}/Thread.java | 4 +--- .../stdlib => opentaint/java/util}/Optional.java | 4 +--- .../java/util/concurrent}/CompletableFuture.java | 4 +--- .../java/util/concurrent}/CompletionStage.java | 4 +--- .../java/util/concurrent}/Executor.java | 4 +--- .../java/util/concurrent}/ExecutorService.java | 4 +--- .../stdlib => opentaint/java/util/stream}/Stream.java | 4 +--- 10 files changed, 16 insertions(+), 31 deletions(-) rename model/java/dataflow/kotlin/src/main/java/{org/opentaint/jvm/dataflow/approximations/kotlin => opentaint/kotlinx/coroutines}/Builders.java (94%) rename model/java/dataflow/kotlin/src/main/java/{org/opentaint/jvm/dataflow/approximations/kotlin => opentaint/kotlinx/coroutines}/BuildersBuilders.java (78%) rename model/java/dataflow/kotlin/src/main/java/{org/opentaint/jvm/dataflow/approximations/kotlin => opentaint/kotlinx/coroutines}/BuildersBuildersCommon.java (93%) rename model/java/dataflow/stdlib/src/main/java/{org/opentaint/jvm/dataflow/approximations/stdlib => opentaint/java/lang}/Thread.java (89%) rename model/java/dataflow/stdlib/src/main/java/{org/opentaint/jvm/dataflow/approximations/stdlib => opentaint/java/util}/Optional.java (94%) rename model/java/dataflow/stdlib/src/main/java/{org/opentaint/jvm/dataflow/approximations/stdlib => opentaint/java/util/concurrent}/CompletableFuture.java (99%) rename model/java/dataflow/stdlib/src/main/java/{org/opentaint/jvm/dataflow/approximations/stdlib => opentaint/java/util/concurrent}/CompletionStage.java (99%) rename model/java/dataflow/stdlib/src/main/java/{org/opentaint/jvm/dataflow/approximations/stdlib => opentaint/java/util/concurrent}/Executor.java (53%) rename model/java/dataflow/stdlib/src/main/java/{org/opentaint/jvm/dataflow/approximations/stdlib => opentaint/java/util/concurrent}/ExecutorService.java (92%) rename model/java/dataflow/stdlib/src/main/java/{org/opentaint/jvm/dataflow/approximations/stdlib => opentaint/java/util/stream}/Stream.java (98%) diff --git a/model/java/dataflow/kotlin/src/main/java/org/opentaint/jvm/dataflow/approximations/kotlin/Builders.java b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/Builders.java similarity index 94% rename from model/java/dataflow/kotlin/src/main/java/org/opentaint/jvm/dataflow/approximations/kotlin/Builders.java rename to model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/Builders.java index b89ced3b4a..9f68e320bc 100644 --- a/model/java/dataflow/kotlin/src/main/java/org/opentaint/jvm/dataflow/approximations/kotlin/Builders.java +++ b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/Builders.java @@ -1,21 +1,20 @@ -package org.opentaint.jvm.dataflow.approximations.kotlin; +package opentaint.kotlinx.coroutines; import kotlin.Unit; import kotlin.coroutines.Continuation; import kotlin.coroutines.CoroutineContext; import kotlin.jvm.functions.Function2; -import kotlinx.coroutines.BuildersKt; import kotlinx.coroutines.CompletableDeferred; import kotlinx.coroutines.CoroutineDispatcher; import kotlinx.coroutines.CoroutineScope; import kotlinx.coroutines.CoroutineStart; import kotlinx.coroutines.Deferred; import kotlinx.coroutines.Job; -import org.opentaint.ir.approximation.annotation.Approximate; +import org.opentaint.ir.approximation.annotation.ApproximatedClassName; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; import org.opentaint.jvm.dataflow.approximations.OpentaintNdUtil; -@Approximate(BuildersKt.class) +@ApproximatedClassName("BuildersKt") public class Builders { public static final T runBlocking(CoroutineContext ctx, @ArgumentTypeContext Function2, ? extends Object> body) { diff --git a/model/java/dataflow/kotlin/src/main/java/org/opentaint/jvm/dataflow/approximations/kotlin/BuildersBuilders.java b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuilders.java similarity index 78% rename from model/java/dataflow/kotlin/src/main/java/org/opentaint/jvm/dataflow/approximations/kotlin/BuildersBuilders.java rename to model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuilders.java index 2190eeaa92..89e15e0c40 100644 --- a/model/java/dataflow/kotlin/src/main/java/org/opentaint/jvm/dataflow/approximations/kotlin/BuildersBuilders.java +++ b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuilders.java @@ -1,13 +1,13 @@ -package org.opentaint.jvm.dataflow.approximations.kotlin; +package opentaint.kotlinx.coroutines; import kotlin.coroutines.Continuation; import kotlin.coroutines.CoroutineContext; import kotlin.jvm.functions.Function2; import kotlinx.coroutines.CoroutineScope; -import org.opentaint.ir.approximation.annotation.ApproximateByName; +import org.opentaint.ir.approximation.annotation.ApproximatedClassName; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; -@ApproximateByName("kotlinx.coroutines.BuildersKt__BuildersKt") +@ApproximatedClassName("BuildersKt__BuildersKt") public class BuildersBuilders { public static final T runBlocking(CoroutineContext ctx, @ArgumentTypeContext Function2, ? extends Object> body) { diff --git a/model/java/dataflow/kotlin/src/main/java/org/opentaint/jvm/dataflow/approximations/kotlin/BuildersBuildersCommon.java b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuildersCommon.java similarity index 93% rename from model/java/dataflow/kotlin/src/main/java/org/opentaint/jvm/dataflow/approximations/kotlin/BuildersBuildersCommon.java rename to model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuildersCommon.java index 190474542f..74d0c756d0 100644 --- a/model/java/dataflow/kotlin/src/main/java/org/opentaint/jvm/dataflow/approximations/kotlin/BuildersBuildersCommon.java +++ b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuildersCommon.java @@ -1,4 +1,4 @@ -package org.opentaint.jvm.dataflow.approximations.kotlin; +package opentaint.kotlinx.coroutines; import kotlin.Unit; import kotlin.coroutines.Continuation; @@ -10,11 +10,11 @@ import kotlinx.coroutines.CoroutineStart; import kotlinx.coroutines.Deferred; import kotlinx.coroutines.Job; -import org.opentaint.ir.approximation.annotation.ApproximateByName; +import org.opentaint.ir.approximation.annotation.ApproximatedClassName; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; import org.opentaint.jvm.dataflow.approximations.OpentaintNdUtil; -@ApproximateByName("kotlinx.coroutines.BuildersKt__Builders_commonKt") +@ApproximatedClassName("BuildersKt__Builders_commonKt") public class BuildersBuildersCommon { public static final Job launch(CoroutineScope scope, CoroutineContext ctx, CoroutineStart start, @ArgumentTypeContext Function2, ? extends Object> body) { diff --git a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Thread.java b/model/java/dataflow/stdlib/src/main/java/opentaint/java/lang/Thread.java similarity index 89% rename from model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Thread.java rename to model/java/dataflow/stdlib/src/main/java/opentaint/java/lang/Thread.java index ffe5f04bc0..3b1cbd155c 100644 --- a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Thread.java +++ b/model/java/dataflow/stdlib/src/main/java/opentaint/java/lang/Thread.java @@ -1,10 +1,8 @@ -package org.opentaint.jvm.dataflow.approximations.stdlib; +package opentaint.java.lang; -import org.opentaint.ir.approximation.annotation.Approximate; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; import org.opentaint.jvm.dataflow.approximations.OpentaintNdUtil; -@Approximate(java.lang.Thread.class) public class Thread { public void start() { diff --git a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Optional.java b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/Optional.java similarity index 94% rename from model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Optional.java rename to model/java/dataflow/stdlib/src/main/java/opentaint/java/util/Optional.java index 9fa8150f7d..1251a1e1e9 100644 --- a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Optional.java +++ b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/Optional.java @@ -1,6 +1,5 @@ -package org.opentaint.jvm.dataflow.approximations.stdlib; +package opentaint.java.util; -import org.opentaint.ir.approximation.annotation.Approximate; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; import java.util.function.Consumer; @@ -8,7 +7,6 @@ import java.util.function.Predicate; import java.util.function.Supplier; -@Approximate(java.util.Optional.class) public class Optional { public void ifPresent(@ArgumentTypeContext Consumer action) { java.util.Optional t = (java.util.Optional) (Object) this; diff --git a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/CompletableFuture.java b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/CompletableFuture.java similarity index 99% rename from model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/CompletableFuture.java rename to model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/CompletableFuture.java index 565a87118b..71dd706437 100644 --- a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/CompletableFuture.java +++ b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/CompletableFuture.java @@ -1,6 +1,5 @@ -package org.opentaint.jvm.dataflow.approximations.stdlib; +package opentaint.java.util.concurrent; -import org.opentaint.ir.approximation.annotation.Approximate; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; import org.opentaint.jvm.dataflow.approximations.OpentaintNdUtil; @@ -12,7 +11,6 @@ import java.util.function.Function; import java.util.function.Supplier; -@Approximate(java.util.concurrent.CompletableFuture.class) public class CompletableFuture { public static java.util.concurrent.CompletableFuture supplyAsync(@ArgumentTypeContext Supplier supplier) { diff --git a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/CompletionStage.java b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/CompletionStage.java similarity index 99% rename from model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/CompletionStage.java rename to model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/CompletionStage.java index 96dcfec2d4..1a605bc715 100644 --- a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/CompletionStage.java +++ b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/CompletionStage.java @@ -1,6 +1,5 @@ -package org.opentaint.jvm.dataflow.approximations.stdlib; +package opentaint.java.util.concurrent; -import org.opentaint.ir.approximation.annotation.Approximate; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; import org.opentaint.jvm.dataflow.approximations.OpentaintNdUtil; @@ -11,7 +10,6 @@ import java.util.function.Consumer; import java.util.function.Function; -@Approximate(java.util.concurrent.CompletionStage.class) public class CompletionStage { public java.util.concurrent.CompletionStage thenApply(@ArgumentTypeContext Function fn) throws Throwable { diff --git a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Executor.java b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/Executor.java similarity index 53% rename from model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Executor.java rename to model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/Executor.java index 2b39b141d6..b9df2d1941 100644 --- a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Executor.java +++ b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/Executor.java @@ -1,9 +1,7 @@ -package org.opentaint.jvm.dataflow.approximations.stdlib; +package opentaint.java.util.concurrent; -import org.opentaint.ir.approximation.annotation.Approximate; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; -@Approximate(java.util.concurrent.Executor.class) public class Executor { public void execute(@ArgumentTypeContext Runnable command) { diff --git a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/ExecutorService.java b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/ExecutorService.java similarity index 92% rename from model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/ExecutorService.java rename to model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/ExecutorService.java index 8de232b17a..f13f7143cd 100644 --- a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/ExecutorService.java +++ b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/concurrent/ExecutorService.java @@ -1,6 +1,5 @@ -package org.opentaint.jvm.dataflow.approximations.stdlib; +package opentaint.java.util.concurrent; -import org.opentaint.ir.approximation.annotation.Approximate; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; import java.util.ArrayList; @@ -12,7 +11,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -@Approximate(java.util.concurrent.ExecutorService.class) public class ExecutorService { public void execute(@ArgumentTypeContext Runnable command) { diff --git a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Stream.java b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/stream/Stream.java similarity index 98% rename from model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Stream.java rename to model/java/dataflow/stdlib/src/main/java/opentaint/java/util/stream/Stream.java index 4c7a37244a..158934148e 100644 --- a/model/java/dataflow/stdlib/src/main/java/org/opentaint/jvm/dataflow/approximations/stdlib/Stream.java +++ b/model/java/dataflow/stdlib/src/main/java/opentaint/java/util/stream/Stream.java @@ -1,6 +1,5 @@ -package org.opentaint.jvm.dataflow.approximations.stdlib; +package opentaint.java.util.stream; -import org.opentaint.ir.approximation.annotation.Approximate; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; import java.util.Comparator; @@ -20,7 +19,6 @@ import java.util.stream.IntStream; import java.util.stream.LongStream; -@Approximate(java.util.stream.Stream.class) public class Stream { private static final int GENERATOR_ARRAY_SIZE = 0; From 9ecce3a73f531625d6655baa13ffbda7a314a494 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Sat, 29 Aug 2026 18:38:00 +0200 Subject: [PATCH 2/6] feat(go-models): model MCP server callbacks --- core/build.gradle.kts | 4 + .../go/go-ir-tests/build.gradle.kts | 4 + .../ir/go/test/features/ModelTests.kt | 108 ++++ .../go/sast/dataflow/McpModelTest.kt | 483 ++++++++++++++++++ ...ithub.com.google.go-github.v89.github.yaml | 9 + ...b.com.modelcontextprotocol.go-sdk.mcp.yaml | 99 ++++ model/go/dataflow/README.md | 21 + .../pkg/inventory/server_tool.go | 32 ++ model/go/dataflow/github-mcp-server/go.mod | 16 + model/go/dataflow/github-mcp-server/go.sum | 24 + .../modelcontextprotocol/go-sdk/mcp/server.go | 88 ++++ .../modelcontextprotocol-go-sdk-v1/go.mod | 16 + .../modelcontextprotocol-go-sdk-v1/go.sum | 24 + 13 files changed, 928 insertions(+) create mode 100644 core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt create mode 100644 model/go/config/github.com.google.go-github.v89.github.yaml create mode 100644 model/go/config/github.com.modelcontextprotocol.go-sdk.mcp.yaml create mode 100644 model/go/dataflow/README.md create mode 100644 model/go/dataflow/github-mcp-server/github.com/github/github-mcp-server/pkg/inventory/server_tool.go create mode 100644 model/go/dataflow/github-mcp-server/go.mod create mode 100644 model/go/dataflow/github-mcp-server/go.sum create mode 100644 model/go/dataflow/modelcontextprotocol-go-sdk-v1/github.com/modelcontextprotocol/go-sdk/mcp/server.go create mode 100644 model/go/dataflow/modelcontextprotocol-go-sdk-v1/go.mod create mode 100644 model/go/dataflow/modelcontextprotocol-go-sdk-v1/go.sum diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 82d4c99bf7..d26e3ce124 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -81,6 +81,10 @@ dependencies { tasks.withType { dependsOn(project("samples").tasks.withType()) ensureGoEnvInitialized() + systemProperty( + "opentaint.go.models.root", + layout.projectDirectory.dir("../model/go/dataflow").asFile.absolutePath, + ) doFirst { val resolvedTestSamples = testSamples.resolve() diff --git a/core/opentaint-ir/go/go-ir-tests/build.gradle.kts b/core/opentaint-ir/go/go-ir-tests/build.gradle.kts index d93d7a9f40..bb2518b59e 100644 --- a/core/opentaint-ir/go/go-ir-tests/build.gradle.kts +++ b/core/opentaint-ir/go/go-ir-tests/build.gradle.kts @@ -21,6 +21,10 @@ tasks.withType().configureEach { dependsOn(":go:buildGoServer") useJUnitPlatform() systemProperty("goir.server.binary", goServerBinary) + systemProperty( + "opentaint.go.models.root", + rootProject.layout.projectDirectory.dir("../../model/go/dataflow").asFile.absolutePath, + ) // Show test output for debugging (started/passed/failed/skipped) testLogging { diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index a9c4337a6e..53b2476712 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -2,6 +2,7 @@ package org.opentaint.ir.go.test.features import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.opentaint.ir.go.api.GoIRApproximatedFunction @@ -38,6 +39,113 @@ import java.nio.file.Path @ExtendWith(GoIRTestExtension::class) class ModelTests { + @Test + fun `GitHub MCP server accepts bundled MCP model`(builder: GoIRTestBuilder) { + val checkout = System.getenv("GITHUB_MCP_SERVER_DIR") + assumeTrue(checkout != null, "GITHUB_MCP_SERVER_DIR is not set") + checkNotNull(checkout) + val modelRoot = Path.of(System.getProperty("opentaint.go.models.root")) + .resolve("modelcontextprotocol-go-sdk-v1") + + val program = builder.buildFromDir( + Path.of(checkout), + GoIRLoadConfig( + mode = GoIRLoadMode.PROJECT, + patterns = listOf("./..."), + modelDirs = listOf(modelRoot), + ), + ) + val inventory = program.findPackage("github.com/github/github-mcp-server/pkg/inventory")!! + val register = inventory.functions.single { it.name == "RegisterFunc" } + val addTool = program.findPackage("github.com/modelcontextprotocol/go-sdk/mcp")!! + .functions.single { + it.name == "AddTool" && it.isMethod && + it.receiverType?.toString()?.contains("Server") == true + } + + assertThat(register.findInstructions()) + .anyMatch { it.call.target?.displayName == addTool.fullName } + assertThat(addTool.syntheticKind).isEqualTo("opentaint model") + assertThat(addTool.findInstructions()) + .anyMatch { it.call.target is GoIRCallTarget.Dynamic } + } + + @Test + fun `bundled MCP model exposes server callbacks`(builder: GoIRTestBuilder) { + val project = project( + "example.com/mcpapp", + """ + package mcpapp + + import ( + "context" + "github.com/modelcontextprotocol/go-sdk/mcp" + ) + + type Input struct { Value string } + type Output struct { Value string } + + func rawHandler(context.Context, *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return nil, nil + } + + func typedHandler(context.Context, *mcp.CallToolRequest, Input) (*mcp.CallToolResult, Output, error) { + return nil, Output{}, nil + } + + func Register(server *mcp.Server) { + server.AddTool(&mcp.Tool{Name: "raw", InputSchema: map[string]any{"type": "object"}}, rawHandler) + mcp.AddTool(server, &mcp.Tool{Name: "typed"}, typedHandler) + } + """.trimIndent(), + ) + val modelRoot = Path.of(System.getProperty("opentaint.go.models.root")) + .resolve("modelcontextprotocol-go-sdk-v1") + project.resolve("go.mod").write( + Files.readString(modelRoot.resolve("go.mod")) + .replaceFirst("module opentaint", "module example.com/mcpapp"), + ) + Files.copy(modelRoot.resolve("go.sum"), project.resolve("go.sum")) + + val program = builder.buildFromDir(project, modelConfig(modelRoot)) + val mcp = program.findPackage("github.com/modelcontextprotocol/go-sdk/mcp")!! + val modeledNames = mcp.functions + .filter { it.bodyAvailable } + .mapTo(mutableSetOf()) { it.name } + + assertThat(modeledNames).contains( + "NewServer", + "AddTool", + "AddPrompt", + "AddResource", + "AddResourceTemplate", + "AddReceivingMiddleware", + "AddSendingMiddleware", + "NewStreamableHTTPHandler", + ) + val rawAddTool = mcp.functions.single { + it.name == "AddTool" && it.isMethod && it.receiverType?.toString()?.contains("Server") == true + } + val newServer = mcp.functions.single { it.name == "NewServer" } + assertThat(newServer.findInstructions()) + .anyMatch { it.call.target is GoIRCallTarget.Dynamic } + assertThat(rawAddTool.syntheticKind).isEqualTo("opentaint model") + assertThat(rawAddTool.findInstructions()) + .anyMatch { it.call.target is GoIRCallTarget.Dynamic } + val genericAddTool = mcp.functions.single { + it.name == "AddTool" && !it.isMethod && it.parent == null + } + assertThat(genericAddTool.bodyAvailable).isTrue() + assertThat(genericAddTool.findInstructions()) + .anyMatch { it.call.target is GoIRCallTarget.Dynamic } + val instantiatedAddTool = mcp.functions.single { + it.fullName.startsWith("github.com/modelcontextprotocol/go-sdk/mcp.AddTool[") + } + assertThat(instantiatedAddTool.syntheticKind).isEqualTo("opentaint model") + assertThat(instantiatedAddTool.findInstructions()) + .anyMatch { it.call.target is GoIRCallTarget.Dynamic } + } + @Test fun `model body replaces dependency body after client deserialization`(builder: GoIRTestBuilder) { val project = Files.createTempDirectory("goir-model-project") diff --git a/core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt new file mode 100644 index 0000000000..80794b1628 --- /dev/null +++ b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt @@ -0,0 +1,483 @@ +package org.opentaint.go.sast.dataflow + +import org.opentaint.common.sast.CommonAnalysisOptions +import org.opentaint.dataflow.configuration.go.serialized.GoNameMatcher +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedAssignAction +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedCondition +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedRule +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedTaintConfig +import org.opentaint.dataflow.configuration.go.serialized.GoSinkMetaData +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Argument +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Result +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier +import org.opentaint.dataflow.go.rules.GoTaintConfiguration +import org.opentaint.go.config.GoDefaultConfigLoader +import org.opentaint.ir.go.api.GoIRProgram +import org.opentaint.ir.go.client.GoIRClient +import org.opentaint.ir.go.client.GoIRLoadConfig +import org.opentaint.ir.go.client.GoIRLoadMode +import org.opentaint.ir.go.ext.findFunctionByFullName +import org.junit.jupiter.api.Assumptions.assumeTrue +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.writeText +import kotlin.test.Test +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.minutes + +class McpModelTest { + @Test + fun `MCP model connects registration to tool handler`() { + val project = Files.createTempDirectory("go-mcp-model") + writeProjectModule(project) + project.resolve("mcp.go").writeText( + """ + package mcpapp + + import ( + "context" + "github.com/modelcontextprotocol/go-sdk/mcp" + ) + + func Source() string { return "" } + func Sink(string) {} + + func toolHandler(context.Context, *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + Sink(Source()) + return nil, nil + } + + func Register(server *mcp.Server) { + server.AddTool( + &mcp.Tool{Name: "test", InputSchema: map[string]any{"type": "object"}}, + toolHandler, + ) + } + """.trimIndent(), + ) + assertModelConnects(project) + } + + @Test + fun `MCP model connects server options to completion handler`() { + val project = Files.createTempDirectory("go-mcp-options-model") + writeProjectModule(project) + project.resolve("mcp.go").writeText( + """ + package mcpapp + + import ( + "context" + "github.com/modelcontextprotocol/go-sdk/mcp" + ) + + func Source() string { return "" } + func Sink(string) {} + + func completionHandler(context.Context, *mcp.CompleteRequest) (*mcp.CompleteResult, error) { + Sink(Source()) + return nil, nil + } + + func Register() { + mcp.NewServer( + &mcp.Implementation{}, + &mcp.ServerOptions{CompletionHandler: completionHandler}, + ) + } + """.trimIndent(), + ) + + assertModelConnects(project) + } + + @Test + fun `MCP callback model and pass-through rule work together`() { + val project = Files.createTempDirectory("go-mcp-pass-through-model") + writeProjectModule(project) + project.resolve("mcp.go").writeText( + """ + package mcpapp + + import ( + "context" + "github.com/modelcontextprotocol/go-sdk/mcp" + ) + + func Source() string { return "" } + func Sink(error) {} + + func toolHandler(context.Context, *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + Sink(mcp.ResourceNotFoundError(Source())) + return nil, nil + } + + func Register(server *mcp.Server) { + server.AddTool(&mcp.Tool{}, toolHandler) + } + """.trimIndent(), + ) + val passThrough = GoDefaultConfigLoader.loadConfig()?.passThrough.orEmpty() + assertTrue( + passThrough.any { + it.function == GoNameMatcher.Simple("ResourceNotFoundError") && + it.pkg == GoNameMatcher.Simple("github.com/modelcontextprotocol/go-sdk/mcp") + }, + ) + + GoIRClient().use { client -> + val withoutModel = client.buildFromDir(project).program + val withModel = client.buildFromDir( + project, + GoIRLoadConfig(modelDirs = listOf(modelPath())), + ).program + + assertTrue(findVulnerabilities(withoutModel, passThrough).isEmpty()) + assertTrue(findVulnerabilities(withModel).isEmpty()) + assertTrue(findVulnerabilities(withModel, passThrough).isNotEmpty()) + } + } + + @Test + fun `MCP tool input is an untrusted callback value`() { + val project = Files.createTempDirectory("go-mcp-input-model") + writeProjectModule(project) + project.resolve("mcp.go").writeText( + """ + package mcpapp + + import ( + "context" + "github.com/modelcontextprotocol/go-sdk/mcp" + ) + + func Sink(string) {} + + func toolHandler( + context.Context, + *mcp.CallToolRequest, + map[string]string, + ) (*mcp.CallToolResult, any, error) { + return nil, nil, nil + } + + func Register(server *mcp.Server) { + mcp.AddTool( + server, + &mcp.Tool{Name: "test"}, + func( + ctx context.Context, + req *mcp.CallToolRequest, + input map[string]string, + ) (*mcp.CallToolResult, any, error) { + Sink(input["command"]) + return toolHandler(ctx, req, input) + }, + ) + } + """.trimIndent(), + ) + + val passThrough = GoDefaultConfigLoader.loadConfig()?.passThrough.orEmpty() + GoIRClient().use { client -> + val withoutModel = client.buildFromDir(project).program + val withModel = client.buildFromDir( + project, + GoIRLoadConfig(modelDirs = listOf(modelPath())), + ).program + + assertTrue(findVulnerabilities(withoutModel, passThrough).isEmpty()) + assertTrue(findVulnerabilities(withModel, passThrough).isNotEmpty()) + } + } + + @Test + fun `MCP callback and pass-through model reach a GitHub workflow mutation`() { + val project = Files.createTempDirectory("go-mcp-github-mutation") + writeProjectModule(project) + writeGoGitHubStub(project) + project.resolve("mcp.go").writeText( + """ + package mcpapp + + import ( + "context" + "strconv" + "github.com/google/go-github/v89/github" + "github.com/modelcontextprotocol/go-sdk/mcp" + ) + + func Register(server *mcp.Server, client *github.Client) { + mcp.AddTool( + server, + &mcp.Tool{Name: "run_workflow"}, + func( + ctx context.Context, + _ *mcp.CallToolRequest, + input map[string]string, + ) (*mcp.CallToolResult, any, error) { + workflowIDText := github.Ptr(input["workflow_id"]) + workflowID, err := strconv.ParseInt(*workflowIDText, 10, 64) + if err != nil { + return nil, nil, err + } + _, err = client.Actions.CreateWorkflowDispatchEventByID( + ctx, + "example", + "repository", + workflowID, + github.CreateWorkflowDispatchEventRequest{}, + ) + return nil, nil, err + }, + ) + } + """.trimIndent(), + ) + + val passThrough = GoDefaultConfigLoader.loadConfig()?.passThrough.orEmpty() + val pointerRules = passThrough.filter { + it.pkg == GoNameMatcher.Simple("github.com/google/go-github/v89/github") && + it.function == GoNameMatcher.Simple("Ptr") + } + val parseRules = passThrough.filter { + it.pkg == GoNameMatcher.Simple("strconv") && + it.function == GoNameMatcher.Simple("ParseInt") + } + assertTrue(pointerRules.isNotEmpty()) + assertTrue(parseRules.isNotEmpty()) + + GoIRClient().use { client -> + val program = client.buildFromDir( + project, + GoIRLoadConfig(modelDirs = listOf(modelPath())), + ).program + assertTrue(findGitHubMutations(program).isEmpty()) + assertTrue(findGitHubMutations(program, pointerRules).isEmpty()) + assertTrue(findGitHubMutations(program, parseRules).isEmpty()) + assertTrue(findGitHubMutations(program, pointerRules + parseRules).isNotEmpty()) + } + } + + @Test + fun `GitHub MCP callback reaches a real workflow mutation`() { + val checkout = System.getenv("GITHUB_MCP_SERVER_DIR") + assumeTrue(checkout != null, "GITHUB_MCP_SERVER_DIR is not set") + checkNotNull(checkout) + + GoIRClient().use { client -> + val program = client.buildFromDir( + Path.of(checkout), + GoIRLoadConfig( + mode = GoIRLoadMode.PROJECT, + patterns = listOf("./..."), + modelDirs = listOf(modelPath(), githubMcpModelPath()), + ), + ).program + val entryPoint = program + .findPackage("github.com/github/github-mcp-server/pkg/github")!! + .functions.single { it.name == "ActionsRunTrigger" } + val inventory = program.findPackage("github.com/github/github-mcp-server/pkg/inventory")!! + assertTrue(inventory.findNamedType("ToolsetMetadata")!!.fields.any { it.name == "ID" }) + assertTrue(inventory.findNamedType("ServerTool")!!.fields.any { it.name == "HandlerFunc" }) + + val requiredPassThrough = GoDefaultConfigLoader.loadConfig()?.passThrough.orEmpty().filter { + it.pkg == GoNameMatcher.Simple("strconv") && + it.function == GoNameMatcher.Simple("ParseInt") + } + assertTrue(requiredPassThrough.isNotEmpty()) + val withoutAdapter = client.buildFromDir( + Path.of(checkout), + GoIRLoadConfig( + mode = GoIRLoadMode.PROJECT, + patterns = listOf("./..."), + modelDirs = listOf(modelPath()), + ), + ).program + assertTrue( + findVulnerabilities( + program = withoutAdapter, + passThrough = requiredPassThrough, + entryPoint = entryPoint.fullName, + sink = githubMutationSink("CreateWorkflowDispatchEventByID", Argument(3)), + timeout = 10.minutes, + ).isEmpty(), + ) + assertTrue( + findVulnerabilities( + program = program, + entryPoint = entryPoint.fullName, + sink = githubMutationSink("CreateWorkflowDispatchEventByID", Argument(3)), + timeout = 10.minutes, + ).isEmpty(), + ) + assertTrue( + findVulnerabilities( + program = program, + passThrough = requiredPassThrough, + entryPoint = entryPoint.fullName, + sink = githubMutationSink("CreateWorkflowDispatchEventByID", Argument(3)), + timeout = 10.minutes, + ).isNotEmpty(), + ) + } + } + + private fun assertModelConnects(project: Path) { + GoIRClient().use { client -> + val withoutModel = client.buildFromDir(project).program + val withModel = client.buildFromDir( + project, + GoIRLoadConfig(modelDirs = listOf(modelPath())), + ).program + + assertTrue(findVulnerabilities(withoutModel).isEmpty()) + assertTrue(findVulnerabilities(withModel).isNotEmpty()) + } + } + + private fun modelPath(): Path = Path.of(System.getProperty("opentaint.go.models.root")) + .resolve("modelcontextprotocol-go-sdk-v1") + + private fun githubMcpModelPath(): Path = Path.of(System.getProperty("opentaint.go.models.root")) + .resolve("github-mcp-server") + + private fun writeProjectModule(project: Path) { + val goMod = Files.readString(modelPath().resolve("go.mod")) + .replaceFirst("module opentaint", "module example.com/mcpapp") + project.resolve("go.mod").writeText(goMod) + Files.copy(modelPath().resolve("go.sum"), project.resolve("go.sum")) + } + + private fun writeGoGitHubStub(project: Path) { + project.resolve("go.mod").writeText( + Files.readString(project.resolve("go.mod")) + + "\nrequire github.com/google/go-github/v89 v89.0.0\n" + + "replace github.com/google/go-github/v89 => ./go-github\n", + ) + val module = project.resolve("go-github") + val pkg = module.resolve("github") + Files.createDirectories(pkg) + module.resolve("go.mod").writeText( + "module github.com/google/go-github/v89\n\ngo 1.25\n", + ) + pkg.resolve("github.go").writeText( + """ + package github + + import "context" + + type CreateWorkflowDispatchEventRequest struct{} + type ActionsService struct{} + type Client struct { Actions *ActionsService } + + func Ptr[T any](value T) *T { return &value } + + func (*ActionsService) CreateWorkflowDispatchEventByID( + context.Context, + string, + string, + int64, + CreateWorkflowDispatchEventRequest, + ) (bool, error) { + return false, nil + } + """.trimIndent(), + ) + } + + private fun findVulnerabilities( + program: GoIRProgram, + passThrough: List = emptyList(), + entryPoint: String = "example.com/mcpapp.Register", + sink: GoSerializedRule.Sink = localSink(), + timeout: kotlin.time.Duration = 1.minutes, + ) = GoTaintAnalyzer( + program, + rules(passThrough, sink), + GoUnitResolver(), + CommonAnalysisOptions(ifdsAnalysisTimeout = timeout).taintAnalyzerOptions(), + ).use { analyzer -> + val entryPointFunction = program.findFunctionByFullName(entryPoint) + ?: error("MCP test entry point is missing") + analyzer.analyzeWithIfds(listOf(entryPointFunction)).first + } + + private fun findGitHubMutations( + program: GoIRProgram, + passThrough: List = emptyList(), + ) = findVulnerabilities( + program = program, + passThrough = passThrough, + sink = githubMutationSink("CreateWorkflowDispatchEventByID", Argument(3)), + ) + + private fun rules( + passThrough: List, + sink: GoSerializedRule.Sink, + ): GoTaintConfiguration { + fun source( + pkg: GoNameMatcher, + function: GoNameMatcher, + ) = GoSerializedRule.Source( + pkg = pkg, + function = function, + condition = null, + taint = listOf( + GoSerializedAssignAction( + "taint", + PositionBaseWithModifiers.WithModifiers(Result, listOf(PositionModifier.AnyField)), + ), + ), + info = null, + ) + return GoTaintConfiguration().also { + it.loadConfig( + GoSerializedTaintConfig( + source = listOf( + source(GoNameMatcher.Simple("example.com/mcpapp"), GoNameMatcher.Simple("Source")), + source( + GoNameMatcher.Pattern( + "(?:github.com/modelcontextprotocol/go-sdk/mcp|" + + "github.com/github/github-mcp-server/pkg/inventory)", + ), + GoNameMatcher.Pattern("opentaintMCPIncoming(?:\\[.*])?"), + ), + ), + sink = listOf(sink), + passThrough = passThrough, + ), + ) + } + } + + private fun localSink() = GoSerializedRule.Sink( + pkg = GoNameMatcher.Simple("example.com/mcpapp"), + function = GoNameMatcher.Simple("Sink"), + condition = GoSerializedCondition.ContainsMark( + "taint", + PositionBaseWithModifiers.BaseOnly(Argument(0)), + ), + trackFactsReachAnalysisEnd = emptyList(), + id = "mcp-model-test", + meta = GoSinkMetaData("MCP model test sink"), + info = null, + ) + + private fun githubMutationSink( + function: String, + position: Argument, + ) = GoSerializedRule.Sink( + pkg = GoNameMatcher.Simple("(*github.com/google/go-github/v89/github.ActionsService)"), + function = GoNameMatcher.Simple(function), + condition = GoSerializedCondition.ContainsMark( + "taint", + PositionBaseWithModifiers.WithModifiers(position, listOf(PositionModifier.AnyField)), + ), + trackFactsReachAnalysisEnd = emptyList(), + id = "mcp-github-privileged-action-test", + meta = GoSinkMetaData("MCP input reaches a GitHub mutation"), + info = null, + ) +} diff --git a/model/go/config/github.com.google.go-github.v89.github.yaml b/model/go/config/github.com.google.go-github.v89.github.yaml new file mode 100644 index 0000000000..2ac480340a --- /dev/null +++ b/model/go/config/github.com.google.go-github.v89.github.yaml @@ -0,0 +1,9 @@ +language: go +passThrough: +- function: + package: github.com/google/go-github/v89/github + name: Ptr + receiver: false + copy: + - from: arg(0) + to: result diff --git a/model/go/config/github.com.modelcontextprotocol.go-sdk.mcp.yaml b/model/go/config/github.com.modelcontextprotocol.go-sdk.mcp.yaml new file mode 100644 index 0000000000..963e81133c --- /dev/null +++ b/model/go/config/github.com.modelcontextprotocol.go-sdk.mcp.yaml @@ -0,0 +1,99 @@ +language: go +passThrough: +- function: + package: github.com/modelcontextprotocol/go-sdk/mcp + name: NewServer + receiver: false + copy: + - from: arg(0) + to: result + - from: arg(1) + to: result +- function: + package: github.com/modelcontextprotocol/go-sdk/mcp + name: AddTool + receiver: false + copy: + - from: arg(1) + to: arg(0) + - from: arg(2) + to: arg(0) +- function: + package: github.com/modelcontextprotocol/go-sdk/mcp + type: Server + name: AddTool + receiver: true + copy: + - from: arg(0) + to: this + - from: arg(1) + to: this +- function: + package: github.com/modelcontextprotocol/go-sdk/mcp + type: Server + name: AddPrompt + receiver: true + copy: + - from: arg(0) + to: this + - from: arg(1) + to: this +- function: + package: github.com/modelcontextprotocol/go-sdk/mcp + type: Server + name: AddResource + receiver: true + copy: + - from: arg(0) + to: this + - from: arg(1) + to: this +- function: + package: github.com/modelcontextprotocol/go-sdk/mcp + type: Server + name: AddResourceTemplate + receiver: true + copy: + - from: arg(0) + to: this + - from: arg(1) + to: this +- function: + package: github.com/modelcontextprotocol/go-sdk/mcp + type: Server + name: AddSendingMiddleware + receiver: true + copy: + - from: arg(0) + to: this +- function: + package: github.com/modelcontextprotocol/go-sdk/mcp + type: Server + name: AddReceivingMiddleware + receiver: true + copy: + - from: arg(0) + to: this +- function: + package: github.com/modelcontextprotocol/go-sdk/mcp + name: NewStreamableHTTPHandler + receiver: false + copy: + - from: arg(0) + to: result + - from: arg(1) + to: result +- function: + package: github.com/modelcontextprotocol/go-sdk/mcp + name: ResourceNotFoundError + receiver: false + copy: + - from: arg(0) + to: result +- function: + package: github.com/modelcontextprotocol/go-sdk/mcp + name: URLElicitationRequiredError + receiver: false + copy: + - from: arg(0) + to: result diff --git a/model/go/dataflow/README.md b/model/go/dataflow/README.md new file mode 100644 index 0000000000..2d6b71873a --- /dev/null +++ b/model/go/dataflow/README.md @@ -0,0 +1,21 @@ +# Built-in Go models + +Each child directory is one Go model module. The analyzer loads these modules by default. + +Use one module for each dependency version line. This rule prevents dependency version +conflicts between models. The model module name must be `opentaint`. Put each model package +under the exact import path of the target package. + +The `modelcontextprotocol-go-sdk-v1` module models server callbacks in MCP Go SDK 1.7.0. +It calls tool, prompt, resource, middleware, server option, and HTTP server factory handlers. +These calls let data flow analysis enter handlers that the SDK stores for a later request. +Before each call, the model converts an untrusted value to the request type. This value marks +the MCP request and the generic tool input as external input. + +The MCP model imports the target SDK. It uses target types in function signatures. It has +one local partial `Server` type because Go requires a local receiver type for a method +declaration. The model does not declare target fields or other target structs. + +The MCP pass-through rules are in `../config`. They model value flow through server setup +and error constructors. The Go model bodies model callback calls. Use both forms together +when a flow enters a stored callback and then passes through an SDK value operation. diff --git a/model/go/dataflow/github-mcp-server/github.com/github/github-mcp-server/pkg/inventory/server_tool.go b/model/go/dataflow/github-mcp-server/github.com/github/github-mcp-server/pkg/inventory/server_tool.go new file mode 100644 index 0000000000..6d73d34257 --- /dev/null +++ b/model/go/dataflow/github-mcp-server/github.com/github/github-mcp-server/pkg/inventory/server_tool.go @@ -0,0 +1,32 @@ +package inventory + +import ( + "context" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// ToolsetMetadata is a partial declaration for the target parameter type. +type ToolsetMetadata struct{} + +// ServerTool is a partial declaration for the target result type. +type ServerTool struct{} + +func NewServerToolWithContextHandler[In any, Out any]( + _ mcp.Tool, + _ ToolsetMetadata, + handler mcp.ToolHandlerFor[In, Out], +) ServerTool { + var ctx context.Context + _, _, _ = handler( + ctx, + opentaintMCPIncoming[*mcp.CallToolRequest](), + opentaintMCPIncoming[In](), + ) + return ServerTool{} +} + +func opentaintMCPIncoming[T any]() T { + var value T + return value +} diff --git a/model/go/dataflow/github-mcp-server/go.mod b/model/go/dataflow/github-mcp-server/go.mod new file mode 100644 index 0000000000..f5b76d3cf3 --- /dev/null +++ b/model/go/dataflow/github-mcp-server/go.mod @@ -0,0 +1,16 @@ +module opentaint + +go 1.25.0 + +require github.com/modelcontextprotocol/go-sdk v1.7.0 + +require ( + github.com/google/jsonschema-go v0.4.3 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/time v0.15.0 // indirect +) diff --git a/model/go/dataflow/github-mcp-server/go.sum b/model/go/dataflow/github-mcp-server/go.sum new file mode 100644 index 0000000000..f93aba1677 --- /dev/null +++ b/model/go/dataflow/github-mcp-server/go.sum @@ -0,0 +1,24 @@ +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44= +github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= diff --git a/model/go/dataflow/modelcontextprotocol-go-sdk-v1/github.com/modelcontextprotocol/go-sdk/mcp/server.go b/model/go/dataflow/modelcontextprotocol-go-sdk-v1/github.com/modelcontextprotocol/go-sdk/mcp/server.go new file mode 100644 index 0000000000..d2da71f1cf --- /dev/null +++ b/model/go/dataflow/modelcontextprotocol-go-sdk-v1/github.com/modelcontextprotocol/go-sdk/mcp/server.go @@ -0,0 +1,88 @@ +package mcp + +import ( + "context" + "net/http" + + target "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// Server is a partial declaration for method receivers. All target fields stay unchanged. +type Server struct{} + +func NewServer(_ *target.Implementation, options *target.ServerOptions) *target.Server { + if options != nil { + var ctx context.Context + options.InitializedHandler(ctx, opentaintMCPIncoming[*target.InitializedRequest]()) + options.RootsListChangedHandler(ctx, opentaintMCPIncoming[*target.RootsListChangedRequest]()) + options.ProgressNotificationHandler(ctx, opentaintMCPIncoming[*target.ProgressNotificationServerRequest]()) + _, _ = options.CompletionHandler(ctx, opentaintMCPIncoming[*target.CompleteRequest]()) + _ = options.SubscribeHandler(ctx, opentaintMCPIncoming[*target.SubscribeRequest]()) + _ = options.UnsubscribeHandler(ctx, opentaintMCPIncoming[*target.UnsubscribeRequest]()) + _ = options.GetSessionID() + } + return nil +} + +func (s *Server) AddPrompt(_ *target.Prompt, handler target.PromptHandler) { + var ctx context.Context + _, _ = handler(ctx, opentaintMCPIncoming[*target.GetPromptRequest]()) +} + +func (s *Server) AddTool(_ *target.Tool, handler target.ToolHandler) { + var ctx context.Context + _, _ = handler(ctx, opentaintMCPIncoming[*target.CallToolRequest]()) +} + +func AddTool[In, Out any]( + _ *target.Server, + _ *target.Tool, + handler target.ToolHandlerFor[In, Out], +) { + var ctx context.Context + _, _, _ = handler(ctx, opentaintMCPIncoming[*target.CallToolRequest](), opentaintMCPIncoming[In]()) +} + +func (s *Server) AddResource(_ *target.Resource, handler target.ResourceHandler) { + var ctx context.Context + _, _ = handler(ctx, opentaintMCPIncoming[*target.ReadResourceRequest]()) +} + +func (s *Server) AddResourceTemplate(_ *target.ResourceTemplate, handler target.ResourceHandler) { + var ctx context.Context + _, _ = handler(ctx, opentaintMCPIncoming[*target.ReadResourceRequest]()) +} + +func (s *Server) AddSendingMiddleware(middleware ...target.Middleware) { + invokeMiddleware(middleware) +} + +func (s *Server) AddReceivingMiddleware(middleware ...target.Middleware) { + invokeMiddleware(middleware) +} + +func NewStreamableHTTPHandler( + getServer func(*http.Request) *target.Server, + _ *target.StreamableHTTPOptions, +) *target.StreamableHTTPHandler { + getServer(opentaintMCPIncoming[*http.Request]()) + return nil +} + +func invokeMiddleware(middleware []target.Middleware) { + var handler target.MethodHandler = emptyMethodHandler + for _, wrap := range middleware { + handler = wrap(handler) + } + var ctx context.Context + _, _ = handler(ctx, opentaintMCPIncoming[string](), opentaintMCPIncoming[target.Request]()) +} + +func opentaintMCPIncoming[T any]() T { + var value T + return value +} + +func emptyMethodHandler(context.Context, string, target.Request) (target.Result, error) { + return nil, nil +} diff --git a/model/go/dataflow/modelcontextprotocol-go-sdk-v1/go.mod b/model/go/dataflow/modelcontextprotocol-go-sdk-v1/go.mod new file mode 100644 index 0000000000..f5b76d3cf3 --- /dev/null +++ b/model/go/dataflow/modelcontextprotocol-go-sdk-v1/go.mod @@ -0,0 +1,16 @@ +module opentaint + +go 1.25.0 + +require github.com/modelcontextprotocol/go-sdk v1.7.0 + +require ( + github.com/google/jsonschema-go v0.4.3 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/time v0.15.0 // indirect +) diff --git a/model/go/dataflow/modelcontextprotocol-go-sdk-v1/go.sum b/model/go/dataflow/modelcontextprotocol-go-sdk-v1/go.sum new file mode 100644 index 0000000000..f93aba1677 --- /dev/null +++ b/model/go/dataflow/modelcontextprotocol-go-sdk-v1/go.sum @@ -0,0 +1,24 @@ +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44= +github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= From dfd44705ae5438dc64cb811408b95dd864d94815 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Sun, 30 Aug 2026 18:14:59 +0200 Subject: [PATCH 3/6] fix(go-models): narrow MCP taint propagation --- .../go/sast/dataflow/McpModelTest.kt | 55 ++++++++++-- ...ithub.com.google.go-github.v89.github.yaml | 4 +- ...b.com.modelcontextprotocol.go-sdk.mcp.yaml | 83 ------------------- 3 files changed, 51 insertions(+), 91 deletions(-) diff --git a/core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt index 80794b1628..d5f61cf1ef 100644 --- a/core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt +++ b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt @@ -23,6 +23,7 @@ import java.nio.file.Files import java.nio.file.Path import kotlin.io.path.writeText import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertTrue import kotlin.time.Duration.Companion.minutes @@ -139,6 +140,30 @@ class McpModelTest { } } + @Test + fun `MCP pass-through rules use only semantic copy paths`() { + val passThrough = GoDefaultConfigLoader.loadConfig()?.passThrough.orEmpty() + val mcpRules = passThrough.filter { + it.pkg == GoNameMatcher.Simple("github.com/modelcontextprotocol/go-sdk/mcp") + } + + assertEquals( + setOf("ResourceNotFoundError", "URLElicitationRequiredError"), + mcpRules.mapTo(mutableSetOf()) { (it.function as GoNameMatcher.Simple).name }, + ) + mcpRules.forEach { rule -> + assertEquals(1, rule.copy.size) + assertEquals( + PositionBaseWithModifiers.BaseOnly(Argument(0)), + rule.copy.single().from, + ) + assertEquals( + PositionBaseWithModifiers.BaseOnly(Result), + rule.copy.single().to, + ) + } + } + @Test fun `MCP tool input is an untrusted callback value`() { val project = Files.createTempDirectory("go-mcp-input-model") @@ -420,29 +445,45 @@ class McpModelTest { fun source( pkg: GoNameMatcher, function: GoNameMatcher, + includeNestedValues: Boolean, ) = GoSerializedRule.Source( pkg = pkg, function = function, condition = null, - taint = listOf( - GoSerializedAssignAction( - "taint", - PositionBaseWithModifiers.WithModifiers(Result, listOf(PositionModifier.AnyField)), - ), - ), + taint = buildList { + add( + GoSerializedAssignAction( + "taint", + PositionBaseWithModifiers.BaseOnly(Result), + ), + ) + if (includeNestedValues) { + add( + GoSerializedAssignAction( + "taint", + PositionBaseWithModifiers.WithModifiers(Result, listOf(PositionModifier.AnyField)), + ), + ) + } + }, info = null, ) return GoTaintConfiguration().also { it.loadConfig( GoSerializedTaintConfig( source = listOf( - source(GoNameMatcher.Simple("example.com/mcpapp"), GoNameMatcher.Simple("Source")), + source( + GoNameMatcher.Simple("example.com/mcpapp"), + GoNameMatcher.Simple("Source"), + includeNestedValues = false, + ), source( GoNameMatcher.Pattern( "(?:github.com/modelcontextprotocol/go-sdk/mcp|" + "github.com/github/github-mcp-server/pkg/inventory)", ), GoNameMatcher.Pattern("opentaintMCPIncoming(?:\\[.*])?"), + includeNestedValues = true, ), ), sink = listOf(sink), diff --git a/model/go/config/github.com.google.go-github.v89.github.yaml b/model/go/config/github.com.google.go-github.v89.github.yaml index 2ac480340a..5667cc9f31 100644 --- a/model/go/config/github.com.google.go-github.v89.github.yaml +++ b/model/go/config/github.com.google.go-github.v89.github.yaml @@ -6,4 +6,6 @@ passThrough: receiver: false copy: - from: arg(0) - to: result + to: + - result + - .# diff --git a/model/go/config/github.com.modelcontextprotocol.go-sdk.mcp.yaml b/model/go/config/github.com.modelcontextprotocol.go-sdk.mcp.yaml index 963e81133c..dde6f58d2e 100644 --- a/model/go/config/github.com.modelcontextprotocol.go-sdk.mcp.yaml +++ b/model/go/config/github.com.modelcontextprotocol.go-sdk.mcp.yaml @@ -1,88 +1,5 @@ language: go passThrough: -- function: - package: github.com/modelcontextprotocol/go-sdk/mcp - name: NewServer - receiver: false - copy: - - from: arg(0) - to: result - - from: arg(1) - to: result -- function: - package: github.com/modelcontextprotocol/go-sdk/mcp - name: AddTool - receiver: false - copy: - - from: arg(1) - to: arg(0) - - from: arg(2) - to: arg(0) -- function: - package: github.com/modelcontextprotocol/go-sdk/mcp - type: Server - name: AddTool - receiver: true - copy: - - from: arg(0) - to: this - - from: arg(1) - to: this -- function: - package: github.com/modelcontextprotocol/go-sdk/mcp - type: Server - name: AddPrompt - receiver: true - copy: - - from: arg(0) - to: this - - from: arg(1) - to: this -- function: - package: github.com/modelcontextprotocol/go-sdk/mcp - type: Server - name: AddResource - receiver: true - copy: - - from: arg(0) - to: this - - from: arg(1) - to: this -- function: - package: github.com/modelcontextprotocol/go-sdk/mcp - type: Server - name: AddResourceTemplate - receiver: true - copy: - - from: arg(0) - to: this - - from: arg(1) - to: this -- function: - package: github.com/modelcontextprotocol/go-sdk/mcp - type: Server - name: AddSendingMiddleware - receiver: true - copy: - - from: arg(0) - to: this -- function: - package: github.com/modelcontextprotocol/go-sdk/mcp - type: Server - name: AddReceivingMiddleware - receiver: true - copy: - - from: arg(0) - to: this -- function: - package: github.com/modelcontextprotocol/go-sdk/mcp - name: NewStreamableHTTPHandler - receiver: false - copy: - - from: arg(0) - to: result - - from: arg(1) - to: result - function: package: github.com/modelcontextprotocol/go-sdk/mcp name: ResourceNotFoundError From 90f3949aee9f8f7bb693e5d95e9cf5de78bd297d Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Wed, 2 Sep 2026 18:56:34 +0200 Subject: [PATCH 4/6] test(go-models): use exact primitive mutation sink --- .../test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt index d5f61cf1ef..11d32952cb 100644 --- a/core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt +++ b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/McpModelTest.kt @@ -514,7 +514,7 @@ class McpModelTest { function = GoNameMatcher.Simple(function), condition = GoSerializedCondition.ContainsMark( "taint", - PositionBaseWithModifiers.WithModifiers(position, listOf(PositionModifier.AnyField)), + PositionBaseWithModifiers.BaseOnly(position), ), trackFactsReachAnalysisEnd = emptyList(), id = "mcp-github-privileged-action-test", From 79bc02093d5aadefe9d302dd743d65b1d9513a76 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Tue, 27 Oct 2026 12:20:23 +0100 Subject: [PATCH 5/6] fix(go-models): restore model builds and callback checks --- .../ir/go/test/features/ModelTests.kt | 39 +++++++++++++++++-- .../samples/build.gradle.kts | 1 + .../{Builders.java => BuildersKt.java} | 4 +- ...lders.java => BuildersKt__BuildersKt.java} | 4 +- ...ava => BuildersKt__Builders_commonKt.java} | 4 +- 5 files changed, 40 insertions(+), 12 deletions(-) rename model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/{Builders.java => BuildersKt.java} (96%) rename model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/{BuildersBuilders.java => BuildersKt__BuildersKt.java} (81%) rename model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/{BuildersBuildersCommon.java => BuildersKt__Builders_commonKt.java} (94%) diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index 53b2476712..db8aa96b31 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -65,7 +65,7 @@ class ModelTests { assertThat(register.findInstructions()) .anyMatch { it.call.target?.displayName == addTool.fullName } - assertThat(addTool.syntheticKind).isEqualTo("opentaint model") + assertThat(addTool).isInstanceOf(GoIRApproximatedFunction::class.java) assertThat(addTool.findInstructions()) .anyMatch { it.call.target is GoIRCallTarget.Dynamic } } @@ -129,7 +129,7 @@ class ModelTests { val newServer = mcp.functions.single { it.name == "NewServer" } assertThat(newServer.findInstructions()) .anyMatch { it.call.target is GoIRCallTarget.Dynamic } - assertThat(rawAddTool.syntheticKind).isEqualTo("opentaint model") + assertThat(rawAddTool).isInstanceOf(GoIRApproximatedFunction::class.java) assertThat(rawAddTool.findInstructions()) .anyMatch { it.call.target is GoIRCallTarget.Dynamic } val genericAddTool = mcp.functions.single { @@ -141,7 +141,7 @@ class ModelTests { val instantiatedAddTool = mcp.functions.single { it.fullName.startsWith("github.com/modelcontextprotocol/go-sdk/mcp.AddTool[") } - assertThat(instantiatedAddTool.syntheticKind).isEqualTo("opentaint model") + assertThat(instantiatedAddTool).isInstanceOf(GoIRApproximatedFunction::class.java) assertThat(instantiatedAddTool.findInstructions()) .anyMatch { it.call.target is GoIRCallTarget.Dynamic } } @@ -799,6 +799,39 @@ class ModelTests { assertThat(call.call.target?.displayName).isEqualTo("net/url.QueryEscape") } + @Test + fun `model field access uses imported target declarations`(builder: GoIRTestBuilder) { + val project = project( + "example.com/options", + """ + package options + type Options struct { Name string; Handler func(string) string } + func Register(options *Options, value string) string { return "original" } + """.trimIndent(), + ) + val model = model( + "example.com/options", + """ + package options + import target "example.com/options" + func Register(options *target.Options, value string) string { + return options.Handler(value) + } + """.trimIndent(), + ) + + val program = builder.buildFromDir(project, modelConfig(model)) + val targetPackage = program.findPackage("example.com/options")!! + val register = targetPackage.functions.single { it.name == "Register" } + val handler = register.findExpressions().single { it.fieldName == "Handler" } + + assertThat(handler.fieldIndex).isEqualTo(1) + assertThat(register.findInstructions()) + .anyMatch { it.call.target is GoIRCallTarget.Dynamic } + assertThat(targetPackage.findNamedType("Options")!!.fields.map { it.name }) + .containsExactly("Name", "Handler") + } + @Test fun `imported target declarations do not damage generic target types`(builder: GoIRTestBuilder) { val project = project( diff --git a/core/opentaint-java-querylang/samples/build.gradle.kts b/core/opentaint-java-querylang/samples/build.gradle.kts index b705dd5f2b..68503a9d28 100644 --- a/core/opentaint-java-querylang/samples/build.gradle.kts +++ b/core/opentaint-java-querylang/samples/build.gradle.kts @@ -6,5 +6,6 @@ tasks { withType { sourceCompatibility = JavaVersion.VERSION_1_8.toString() targetCompatibility = JavaVersion.VERSION_1_8.toString() + options.encoding = "UTF-8" } } diff --git a/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/Builders.java b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersKt.java similarity index 96% rename from model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/Builders.java rename to model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersKt.java index 9f68e320bc..7842b5430f 100644 --- a/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/Builders.java +++ b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersKt.java @@ -10,12 +10,10 @@ import kotlinx.coroutines.CoroutineStart; import kotlinx.coroutines.Deferred; import kotlinx.coroutines.Job; -import org.opentaint.ir.approximation.annotation.ApproximatedClassName; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; import org.opentaint.jvm.dataflow.approximations.OpentaintNdUtil; -@ApproximatedClassName("BuildersKt") -public class Builders { +public class BuildersKt { public static final T runBlocking(CoroutineContext ctx, @ArgumentTypeContext Function2, ? extends Object> body) { return (T) body.invoke(null, null); diff --git a/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuilders.java b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersKt__BuildersKt.java similarity index 81% rename from model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuilders.java rename to model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersKt__BuildersKt.java index 89e15e0c40..636536ed35 100644 --- a/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuilders.java +++ b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersKt__BuildersKt.java @@ -4,11 +4,9 @@ import kotlin.coroutines.CoroutineContext; import kotlin.jvm.functions.Function2; import kotlinx.coroutines.CoroutineScope; -import org.opentaint.ir.approximation.annotation.ApproximatedClassName; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; -@ApproximatedClassName("BuildersKt__BuildersKt") -public class BuildersBuilders { +public class BuildersKt__BuildersKt { public static final T runBlocking(CoroutineContext ctx, @ArgumentTypeContext Function2, ? extends Object> body) { return (T) body.invoke(null, null); diff --git a/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuildersCommon.java b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersKt__Builders_commonKt.java similarity index 94% rename from model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuildersCommon.java rename to model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersKt__Builders_commonKt.java index 74d0c756d0..85e97d568a 100644 --- a/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersBuildersCommon.java +++ b/model/java/dataflow/kotlin/src/main/java/opentaint/kotlinx/coroutines/BuildersKt__Builders_commonKt.java @@ -10,12 +10,10 @@ import kotlinx.coroutines.CoroutineStart; import kotlinx.coroutines.Deferred; import kotlinx.coroutines.Job; -import org.opentaint.ir.approximation.annotation.ApproximatedClassName; import org.opentaint.jvm.dataflow.approximations.ArgumentTypeContext; import org.opentaint.jvm.dataflow.approximations.OpentaintNdUtil; -@ApproximatedClassName("BuildersKt__Builders_commonKt") -public class BuildersBuildersCommon { +public class BuildersKt__Builders_commonKt { public static final Job launch(CoroutineScope scope, CoroutineContext ctx, CoroutineStart start, @ArgumentTypeContext Function2, ? extends Object> body) { if (OpentaintNdUtil.nextBool()) return null; From 3b95d5755f7db6326e6959251c61e24db6cbc240 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Tue, 27 Oct 2026 13:15:04 +0100 Subject: [PATCH 6/6] test(go-models): load bundled models into unrelated projects --- .../ir/go/test/features/ModelTests.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt index db8aa96b31..02b3370782 100644 --- a/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt +++ b/core/opentaint-ir/go/go-ir-tests/src/test/kotlin/org/opentaint/ir/go/test/features/ModelTests.kt @@ -735,6 +735,25 @@ class ModelTests { assertThat((callTarget as GoIRCallTarget.Function).function).isSameAs(helper) } + @Test + fun `bundled models accept an unrelated Go project`(builder: GoIRTestBuilder) { + val project = project("example.com/unrelated", "package unrelated\nfunc Target() {}\n") + val modelRoot = Path.of(System.getProperty("opentaint.go.models.root")) + val models = Files.list(modelRoot).use { paths -> + paths.iterator().asSequence().filter { Files.isRegularFile(it.resolve("go.mod")) }.toList() + } + assertThat(models).isNotEmpty() + + val program = builder.buildFromDir( + project, + GoIRLoadConfig(mode = GoIRLoadMode.PROJECT, modelDirs = models), + ) + + val withoutModels = builder.buildFromDir(project, GoIRLoadConfig(mode = GoIRLoadMode.PROJECT)) + assertThat(program.findPackage("example.com/unrelated")).isNotNull() + assertThat(program.packages.keys).containsExactlyInAnyOrderElementsOf(withoutModels.packages.keys) + } + @Test fun `unrelated model does not add packages to the analyzed project`(builder: GoIRTestBuilder) { val project = project("example.com/unrelated", "package unrelated\nfunc Target() {}\n")