From bec0a456e4c1fec5c94b233001a691f60bc353ee Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 11 Sep 2025 10:20:45 +0200 Subject: [PATCH 01/93] Chat interface for AI assistant --- CLAUDE.md | 132 +++ codyze-console/build.gradle.kts | 11 + .../aisec/codyze/console/ChatService.kt | 215 ++++ .../fraunhofer/aisec/codyze/console/Main.kt | 26 +- .../fraunhofer/aisec/codyze/console/Nodes.kt | 2 +- .../fraunhofer/aisec/codyze/console/Router.kt | 19 +- .../src/main/resources/application.conf | 12 + codyze-console/src/main/webapp/pnpm-lock.yaml | 1013 ++++++++--------- .../ai-assistant/ChatInterface.svelte | 216 ++++ .../ai-assistant/WelcomeScreen.svelte | 124 ++ .../src/lib/components/ai-assistant/index.ts | 3 + .../lib/components/analysis/CodeViewer.svelte | 126 ++ .../lib/components/analysis/FileTree.svelte | 162 +++ .../src/lib/components/analysis/index.ts | 2 + .../lib/components/navigation/Sidebar.svelte | 9 +- .../webapp/src/lib/services/apiService.ts | 122 ++ .../main/webapp/src/lib/services/llmAgent.ts | 30 + .../src/routes/ai-assistant/+page.svelte | 157 +++ .../webapp/src/routes/ai-assistant/+page.ts | 16 + codyze-console/src/main/webapp/vite.config.ts | 7 +- cpg-mcp/build.gradle.kts | 5 +- .../fraunhofer/aisec/cpg/mcp/Application.kt | 50 +- .../aisec/cpg/mcp/mcpserver/McpServer.kt | 19 +- .../mcp/mcpserver/tools/utils/Serializable.kt | 30 + 24 files changed, 1913 insertions(+), 595 deletions(-) create mode 100644 CLAUDE.md create mode 100644 codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt create mode 100644 codyze-console/src/main/resources/application.conf create mode 100644 codyze-console/src/main/webapp/src/lib/components/ai-assistant/ChatInterface.svelte create mode 100644 codyze-console/src/main/webapp/src/lib/components/ai-assistant/WelcomeScreen.svelte create mode 100644 codyze-console/src/main/webapp/src/lib/components/ai-assistant/index.ts create mode 100644 codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte create mode 100644 codyze-console/src/main/webapp/src/lib/components/analysis/FileTree.svelte create mode 100644 codyze-console/src/main/webapp/src/lib/services/apiService.ts create mode 100644 codyze-console/src/main/webapp/src/lib/services/llmAgent.ts create mode 100644 codyze-console/src/main/webapp/src/routes/ai-assistant/+page.svelte create mode 100644 codyze-console/src/main/webapp/src/routes/ai-assistant/+page.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..d1558c77444 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,132 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Building the Project +```bash +# Full build with formatting and tests +./gradlew clean spotlessApply build publishToMavenLocal + +# Quick build +./gradlew build + +# Build specific module +./gradlew :cpg-core:build +./gradlew :codyze-console:build +``` + +### Code Quality and Testing +```bash +# Run all tests +./gradlew test + +# Run integration tests +./gradlew integrationTest + +# Run performance tests +./gradlew performanceTest + +# Apply code formatting (Google Java Style) +./gradlew spotlessApply + +# Check code formatting +./gradlew spotlessCheck + +# Generate test coverage report +./gradlew jacocoTestReport +``` + +### Frontend Development (codyze-console) +```bash +# Navigate to webapp directory first +cd codyze-console/src/main/webapp + +# Install dependencies +pnpm install + +# Development server +pnpm run dev + +# Build frontend +pnpm run build + +# Type checking +pnpm run check + +# Lint and format +pnpm run lint +pnpm run format +``` + +### Backend Development (codyze-console) +```bash +# Compile Kotlin backend +./gradlew :codyze-console:compileKotlin --console=plain +``` + +### MCP Server Development (cpg-mcp) +```bash +# Build and install MCP server +./gradlew :cpg-mcp:installDist + +# Run MCP server (stdio mode for Claude Desktop) +./gradlew :cpg-mcp:run + +# Run MCP server with SSE mode on port 8080 +./gradlew :cpg-mcp:run --args="--sse 8080" +``` + +## Project Architecture + +### Core Structure +- **cpg-mcp**: MCP (Model Context Protocol) server providing CPG analysis tools for LLM integration +- **codyze-console**: Kotlin backend with Svelte 5 frontend for web-based analysis +- **cpg-core**: Core library containing AST nodes, graph structures, passes, and type system +- **cpg-concepts**: Concept and operation definitions + +### Technology Stack + +#### Backend (cpg-core, codyze modules) +- **Language**: Kotlin (requires Java 21) +- **Build**: Gradle with Kotlin DSL +- **Testing**: JUnit 5, kotlin.test, Mockk +- **Formatting**: Google Java Style (spotless plugin) + +#### Frontend (codyze-console webapp) +- **Framework**: Svelte 5 with SvelteKit +- **Styling**: Tailwind CSS +- **Package Manager**: pnpm (required, not npm) +- **Charts**: Chart.js + +### Key Development Patterns + +#### Svelte 5 Runes (codyze-console frontend) +```javascript +// Reactive state +let count = $state(0); + +// Derived values +const doubled = $derived(count * 2); + +// Effects +$effect(() => { + console.log('Count changed:', count); +}); + +// Component props +let { items }: Props = $props(); +``` + +**Available MCP Tools:** +- `cpg_analyze` - Parse code files to build CPG +- `cpg_llm_analyze` - Generate LLM prompts for concept/operation suggestions +- `cpg_apply_concepts` - Apply suggested concepts/operations to nodes +- `cpg_dataflow` - Perform dataflow analysis +- `list_functions` - List function declarations +- `list_records` - List record declarations (classes, structs) +- `list_calls` - List call expressions +- `list_calls_to` - List calls to specific functions +- `list_available_concepts` - List available concepts +- `list_available_operations` - List available operations \ No newline at end of file diff --git a/codyze-console/build.gradle.kts b/codyze-console/build.gradle.kts index 21eb593e9fd..6af353b0583 100644 --- a/codyze-console/build.gradle.kts +++ b/codyze-console/build.gradle.kts @@ -17,14 +17,25 @@ mavenPublishing { dependencies { // CPG modules implementation(projects.cpgConcepts) + implementation(projects.cpgMcp) + implementation(libs.mcp) // Ktor server dependencies implementation(libs.bundles.ktor) + // Ktor client dependencies + implementation("io.ktor:ktor-client-core:${libs.versions.ktor.get()}") + implementation("io.ktor:ktor-client-cio:${libs.versions.ktor.get()}") + implementation("io.ktor:ktor-client-content-negotiation:${libs.versions.ktor.get()}") + // Serialization implementation(libs.kotlinx.serialization.json) implementation(libs.jacksonyml) + implementation("dev.langchain4j:langchain4j-mcp:1.4.0-beta10") + implementation("dev.langchain4j:langchain4j:1.4.0") + implementation("dev.langchain4j:langchain4j-ollama:1.4.0") + // Testing testImplementation(libs.ktor.server.test.host) testImplementation(libs.ktor.client.content.negotiation) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt new file mode 100644 index 00000000000..9a269d0d45f --- /dev/null +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt @@ -0,0 +1,215 @@ +/* + * 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.console + +import com.typesafe.config.ConfigFactory +import de.fraunhofer.aisec.cpg.mcp.mcpserver.configureServer +import de.fraunhofer.aisec.cpg.mcp.mcpserver.listTools +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.utils.io.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.jsonObject + +@Serializable data class ChatMessageJSON(val role: String, val content: String) + +@Serializable data class ChatRequestJSON(val messages: List) + +enum class LLMProvider { + OLLAMA, + OPENAI, + ANTHROPIC, +} + +@Serializable +private data class LLMRequest( + val model: String, + val messages: List, + val maxTokens: Int? = null, + val stream: Boolean = false, + val tools: List? = null, +) + +@Serializable private data class MCPToolCall(val function: MCPToolCallFunction) + +@Serializable private data class MCPToolCallFunction(val name: String, val arguments: JsonObject) + +@Serializable +private data class LLMResponse( + val model: String? = null, + val message: LLMMessage? = null, + val done: Boolean? = null, +) + +@Serializable +private data class LLMMessage( + val role: String? = null, + val content: String? = null, + val tool_calls: List? = null, +) + +class ChatService { + private val httpClient = + HttpClient(CIO) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } + val config = ConfigFactory.load() + + val provider = LLMProvider.valueOf(config.getString("llm.provider").uppercase()) + val baseUrl = config.getString("llm.baseUrl") + val model = config.getString("llm.model") + + // MCP Server Integration + private val mcpServer = configureServer() + + /** Get available tools from MCP server (tools/list) */ + private fun getAvailableTools(): List { + return mcpServer.listTools().map { registeredTool -> + Json.encodeToJsonElement(registeredTool).jsonObject + } + } + + /** Execute MCP tool call (tools/call) */ + private fun executeToolCall(toolCall: MCPToolCall): String { + return try { + val toolName = toolCall.function.name + val mcpTool = mcpServer.tools[toolName] + + if (mcpTool != null) { + // TODO: Invoke tool call properly + // val result = mcpTool.handler.invoke(toolCall.function.arguments) + // For now, simulate execution + "Tool '$toolName' executed with args: ${toolCall.function.arguments}" + } else { + "Error: Tool '$toolName' not found" + } + } catch (e: Exception) { + "Error executing tool '${toolCall.function.name}': ${e.message}" + } + } + + fun chat(request: ChatRequestJSON): Flow = flow { + if (baseUrl.isNullOrEmpty()) { + emit("{\"error\":\"LLM configuration missing: LLM_BASE_URL not set\"}") + return@flow + } + + if (model.isNullOrEmpty()) { + emit("{\"error\":\"LLM configuration missing: LLM_MODEL not set\"}") + return@flow + } + + try { + when (provider) { + LLMProvider.OLLAMA -> { + chatOllamaStream(request.messages).collect { chunk -> emit(chunk) } + } + + LLMProvider.OPENAI -> TODO() + LLMProvider.ANTHROPIC -> TODO() + } + } catch (e: Exception) { + emit("{\"error\":\"Chat error: ${e.message}\"}") + } + } + + private fun chatOllamaStream(messages: List): Flow = flow { + try { + // Get available MCP tools + val availableTools = getAvailableTools() + + val response = + httpClient.post("$baseUrl/api/chat") { + contentType(ContentType.Application.Json) + setBody( + LLMRequest( + model = model!!, + messages = messages, + stream = true, + tools = availableTools.ifEmpty { null }, + ) + ) + } + + if (response.status.isSuccess()) { + val channel = response.bodyAsChannel() + val jsonParser = Json { ignoreUnknownKeys = true } + + while (!channel.isClosedForRead) { + val chunk = channel.readUTF8Line() + if (chunk != null && chunk.isNotBlank()) { + try { + val llmResponse = jsonParser.decodeFromString(chunk) + + if (llmResponse.done == true) { + break + } + + val message = llmResponse.message + + // Handle text content + val content = message?.content + if (!content.isNullOrEmpty()) { + emit(content) + } + + // Handle tool calls + val toolCalls = message?.tool_calls + if (!toolCalls.isNullOrEmpty()) { + emit("\n\nšŸ”§ **Tools Called:**\n") + + for (toolCall in toolCalls) { + emit("- **${toolCall.function.name}**\n") + val result = executeToolCall(toolCall) + emit(" Result: $result\n") + } + + emit("\n") + } + } catch (e: Exception) { + // Skip invalid JSON lines + continue + } + } + } + } else { + emit( + "{\"error\":\"Ollama request failed: ${response.status} - Check if Ollama server is running at $baseUrl\"}" + ) + } + } catch (e: Exception) { + emit("{\"error\":\"Failed to connect to Ollama server at $baseUrl: ${e.message}\"}") + } + } +} diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt index c1e17de5518..7b27b30f2cb 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt @@ -40,9 +40,9 @@ import kotlinx.serialization.json.Json * listens on localhost at port 8080. The server is configured using the [configureWebconsole] * function. */ -fun ConsoleService.startConsole() { +fun ConsoleService.startConsole(chatService: ChatService = ChatService()) { embeddedServer(Netty, host = "localhost", port = 8080) { - configureWebconsole(this@startConsole) + configureWebconsole(this@startConsole, chatService) } .start(wait = true) } @@ -54,7 +54,10 @@ fun ConsoleService.startConsole() { * Note: Currently, the CORS policy allows any host. This should be restricted to specific hosts in * a production environment and will be made available as an option later. */ -fun Application.configureWebconsole(service: ConsoleService = ConsoleService()) { +fun Application.configureWebconsole( + service: ConsoleService = ConsoleService(), + chatService: ChatService = ChatService(), +) { install(CORS) { anyHost() allowHeader(HttpHeaders.ContentType) @@ -69,17 +72,28 @@ fun Application.configureWebconsole(service: ConsoleService = ConsoleService()) ) } - configureRouting(service) + configureRouting(service, chatService) } /** * This function sets up the routing for the web console. It defines the API routes and static * resources (for serving the single-page application frontend). */ -fun Application.configureRouting(service: ConsoleService) { +fun Application.configureRouting( + service: ConsoleService, + chatService: ChatService = ChatService(), +) { routing { // We'll add routes here - apiRoutes(service) + apiRoutes(service, chatService) frontendRoutes() } } + +fun Application.module() { + val config = environment.config + + val provider = config.property("llm.provider").getString() + val baseUrl = config.property("llm.baseUrl").getString() + val model = config.property("llm.model").getString() +} diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Nodes.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Nodes.kt index 578508ee954..6d7c8cc7240 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Nodes.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Nodes.kt @@ -310,7 +310,7 @@ fun AnalysisResult.toJSON(): AnalysisResultJSON = components = components.map { it.toJSON() }, totalNodes = nodes.size, analysisResult = this@toJSON, - sourceDir = config.sourceLocations.first().absolutePath, + sourceDir = config.sourceLocations.firstOrNull()?.absolutePath ?: "", findings = sarif.runs.flatMap { it.results?.map { it.toJSON() } ?: emptyList() }, requirementCategories = project.requirementCategoriesToJSON(this@toJSON.requirementsResults), diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt index a210f4f63fe..39bb3c7e685 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt @@ -59,7 +59,7 @@ import kotlin.reflect.KClass * - POST `/api/concept`: Adds a concept node to the current * [de.fraunhofer.aisec.codyze.AnalysisResult] */ -fun Routing.apiRoutes(service: ConsoleService) { +fun Routing.apiRoutes(service: ConsoleService, chatService: ChatService) { // The API routes are prefixed with /api route("/api") { // The endpoint to analyze a project @@ -245,6 +245,23 @@ fun Routing.apiRoutes(service: ConsoleService) { } } + post("/chat") { + try { + val request = call.receive() + call.respondTextWriter(contentType = ContentType.Text.EventStream) { + chatService.chat(request).collect { chunk -> + write("data: $chunk\n\n") + flush() + } + } + } catch (e: Exception) { + call.respond( + HttpStatusCode.InternalServerError, + mapOf("error" to "Chat request failed: ${e.message}"), + ) + } + } + // The endpoint to get a QueryTree with its parent IDs for tree expansion get("/querytrees/{queryTreeId}/parents") { val queryTreeId = diff --git a/codyze-console/src/main/resources/application.conf b/codyze-console/src/main/resources/application.conf new file mode 100644 index 00000000000..d7d8329a150 --- /dev/null +++ b/codyze-console/src/main/resources/application.conf @@ -0,0 +1,12 @@ +llm { + # LLM Configuration + provider = "OLLAMA" # Options: OLLAMA, OPENAI, ANTHROPIC + baseUrl = "http://172.31.6.131:11434" + model = "gpt-oss:120b" + apiKey = "" # Required for OPENAI and ANTHROPIC, optional for OLLAMA +} + +mcp { + # MCP Server Configuration + serverUrl = "http://localhost:8081" +} \ No newline at end of file diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 740e17d405a..12ffe86cc4c 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -14,86 +14,82 @@ importers: devDependencies: '@eslint/compat': specifier: ^1.2.5 - version: 1.3.0(eslint@9.27.0(jiti@2.4.2)) + version: 1.3.2(eslint@9.34.0(jiti@2.5.1)) '@eslint/js': specifier: ^9.18.0 - version: 9.27.0 + version: 9.34.0 '@fontsource/noto-sans-mono': specifier: ^5.2.6 version: 5.2.7 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.8(@sveltejs/kit@2.37.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) + version: 3.0.9(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1))) '@sveltejs/kit': specifier: ^2.20.6 - version: 2.37.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 2.37.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 - version: 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 5.1.1(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)) '@tailwindcss/forms': specifier: ^0.5.10 - version: 0.5.10(tailwindcss@4.1.7) + version: 0.5.10(tailwindcss@4.1.12) '@tailwindcss/typography': specifier: ^0.5.15 - version: 0.5.16(tailwindcss@4.1.7) + version: 0.5.16(tailwindcss@4.1.12) '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.1.7(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 4.1.12(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)) cookie: specifier: 1.0.2 version: 1.0.2 eslint: specifier: ^9.18.0 - version: 9.27.0(jiti@2.4.2) + version: 9.34.0(jiti@2.5.1) eslint-config-prettier: specifier: ^10.0.1 - version: 10.1.5(eslint@9.27.0(jiti@2.4.2)) + version: 10.1.8(eslint@9.34.0(jiti@2.5.1)) eslint-plugin-svelte: specifier: ^3.0.0 - version: 3.11.0(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4) + version: 3.12.0(eslint@9.34.0(jiti@2.5.1))(svelte@5.38.6) globals: specifier: ^16.0.0 version: 16.3.0 inter-ui: specifier: ^4.1.0 - version: 4.1.0 + version: 4.1.1 prettier: specifier: ^3.5.3 - version: 3.5.3 + version: 3.6.2 prettier-plugin-svelte: specifier: ^3.3.3 - version: 3.4.0(prettier@3.5.3)(svelte@5.33.4) + version: 3.4.0(prettier@3.6.2)(svelte@5.38.6) prettier-plugin-tailwindcss: specifier: ^0.6.11 - version: 0.6.11(prettier-plugin-svelte@3.4.0(prettier@3.5.3)(svelte@5.33.4))(prettier@3.5.3) + version: 0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.38.6))(prettier@3.6.2) svelte: specifier: ^5.0.0 - version: 5.33.4 + version: 5.38.6 svelte-check: specifier: ^4.0.0 - version: 4.2.1(picomatch@4.0.3)(svelte@5.33.4)(typescript@5.8.3) + version: 4.3.1(picomatch@4.0.3)(svelte@5.38.6)(typescript@5.9.2) svelte-highlight: specifier: ^7.8.2 - version: 7.8.3 + version: 7.8.4 tailwindcss: specifier: ^4.0.0 - version: 4.1.7 + version: 4.1.12 typescript: specifier: ^5.0.0 - version: 5.8.3 + version: 5.9.2 typescript-eslint: specifier: ^8.20.0 - version: 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) vite: specifier: ^6.0.0 - version: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) + version: 6.3.5(jiti@2.5.1)(lightningcss@1.30.1) packages: - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@esbuild/aix-ppc64@0.25.9': resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} @@ -250,14 +246,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.5.1': - resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + '@eslint-community/eslint-utils@4.8.0': + resolution: {integrity: sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -266,41 +256,41 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/compat@1.3.0': - resolution: {integrity: sha512-ZBygRBqpDYiIHsN+d1WyHn3TYgzgpzLEcgJUxTATyiInQbKZz6wZb6+ljwdg8xeeOe4v03z6Uh6lELiw0/mVhQ==} + '@eslint/compat@1.3.2': + resolution: {integrity: sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^9.10.0 + eslint: ^8.40 || 9 peerDependenciesMeta: eslint: optional: true - '@eslint/config-array@0.20.0': - resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.1': - resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.14.0': - resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.27.0': - resolution: {integrity: sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==} + '@eslint/js@9.34.0': + resolution: {integrity: sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.1': - resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@fontsource/noto-sans-mono@5.2.7': @@ -322,37 +312,29 @@ packages: resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} - '@humanwhocodes/retry@0.4.2': - resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - '@jridgewell/sourcemap-codec@1.5.4': - resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} @@ -372,108 +354,108 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@rollup/rollup-android-arm-eabi@4.50.1': - resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} + '@rollup/rollup-android-arm-eabi@4.50.0': + resolution: {integrity: sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.50.1': - resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} + '@rollup/rollup-android-arm64@4.50.0': + resolution: {integrity: sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.50.1': - resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} + '@rollup/rollup-darwin-arm64@4.50.0': + resolution: {integrity: sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.50.1': - resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} + '@rollup/rollup-darwin-x64@4.50.0': + resolution: {integrity: sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.50.1': - resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} + '@rollup/rollup-freebsd-arm64@4.50.0': + resolution: {integrity: sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.50.1': - resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} + '@rollup/rollup-freebsd-x64@4.50.0': + resolution: {integrity: sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.50.1': - resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} + '@rollup/rollup-linux-arm-gnueabihf@4.50.0': + resolution: {integrity: sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.50.1': - resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} + '@rollup/rollup-linux-arm-musleabihf@4.50.0': + resolution: {integrity: sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.50.1': - resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} + '@rollup/rollup-linux-arm64-gnu@4.50.0': + resolution: {integrity: sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.50.1': - resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} + '@rollup/rollup-linux-arm64-musl@4.50.0': + resolution: {integrity: sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.50.1': - resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} + '@rollup/rollup-linux-loongarch64-gnu@4.50.0': + resolution: {integrity: sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.50.1': - resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} + '@rollup/rollup-linux-ppc64-gnu@4.50.0': + resolution: {integrity: sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.50.1': - resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} + '@rollup/rollup-linux-riscv64-gnu@4.50.0': + resolution: {integrity: sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.50.1': - resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} + '@rollup/rollup-linux-riscv64-musl@4.50.0': + resolution: {integrity: sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.50.1': - resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} + '@rollup/rollup-linux-s390x-gnu@4.50.0': + resolution: {integrity: sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.50.1': - resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} + '@rollup/rollup-linux-x64-gnu@4.50.0': + resolution: {integrity: sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.50.1': - resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} + '@rollup/rollup-linux-x64-musl@4.50.0': + resolution: {integrity: sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.50.1': - resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} + '@rollup/rollup-openharmony-arm64@4.50.0': + resolution: {integrity: sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.50.1': - resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} + '@rollup/rollup-win32-arm64-msvc@4.50.0': + resolution: {integrity: sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.50.1': - resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} + '@rollup/rollup-win32-ia32-msvc@4.50.0': + resolution: {integrity: sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.50.1': - resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} + '@rollup/rollup-win32-x64-msvc@4.50.0': + resolution: {integrity: sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==} cpu: [x64] os: [win32] @@ -485,13 +467,13 @@ packages: peerDependencies: acorn: ^8.9.0 - '@sveltejs/adapter-static@3.0.8': - resolution: {integrity: sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==} + '@sveltejs/adapter-static@3.0.9': + resolution: {integrity: sha512-aytHXcMi7lb9ljsWUzXYQ0p5X1z9oWud2olu/EpmH7aCu4m84h7QLvb5Wp+CFirKcwoNnYvYWhyP/L8Vh1ztdw==} peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.37.1': - resolution: {integrity: sha512-4T9rF2Roe7RGvHfcn6+n92Yc2NF88k7ljFz9+wE0jWxyencqRpadr2/CvlcQbbTXf1ozmFxgMO6af+qm+1mPFw==} + '@sveltejs/kit@2.37.0': + resolution: {integrity: sha512-xgKtpjQ6Ry4mdShd01ht5AODUsW7+K1iValPDq7QX8zI1hWOKREH9GjG8SRCN5tC4K7UXmMhuQam7gbLByVcnw==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -511,8 +493,8 @@ packages: svelte: ^5.0.0 vite: ^6.0.0 - '@sveltejs/vite-plugin-svelte@5.1.0': - resolution: {integrity: sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==} + '@sveltejs/vite-plugin-svelte@5.1.1': + resolution: {integrity: sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22} peerDependencies: svelte: ^5.0.0 @@ -523,65 +505,65 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' - '@tailwindcss/node@4.1.7': - resolution: {integrity: sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==} + '@tailwindcss/node@4.1.12': + resolution: {integrity: sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==} - '@tailwindcss/oxide-android-arm64@4.1.7': - resolution: {integrity: sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==} + '@tailwindcss/oxide-android-arm64@4.1.12': + resolution: {integrity: sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.7': - resolution: {integrity: sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==} + '@tailwindcss/oxide-darwin-arm64@4.1.12': + resolution: {integrity: sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.7': - resolution: {integrity: sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==} + '@tailwindcss/oxide-darwin-x64@4.1.12': + resolution: {integrity: sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.7': - resolution: {integrity: sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==} + '@tailwindcss/oxide-freebsd-x64@4.1.12': + resolution: {integrity: sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': - resolution: {integrity: sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': + resolution: {integrity: sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': - resolution: {integrity: sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': + resolution: {integrity: sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.7': - resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.12': + resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.7': - resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.12': + resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.7': - resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==} + '@tailwindcss/oxide-linux-x64-musl@4.1.12': + resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.7': - resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==} + '@tailwindcss/oxide-wasm32-wasi@4.1.12': + resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -592,20 +574,20 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': - resolution: {integrity: sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': + resolution: {integrity: sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.7': - resolution: {integrity: sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.12': + resolution: {integrity: sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.7': - resolution: {integrity: sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==} + '@tailwindcss/oxide@4.1.12': + resolution: {integrity: sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==} engines: {node: '>= 10'} '@tailwindcss/typography@0.5.16': @@ -613,78 +595,77 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' - '@tailwindcss/vite@4.1.7': - resolution: {integrity: sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==} + '@tailwindcss/vite@4.1.12': + resolution: {integrity: sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==} peerDependencies: - vite: ^5.2.0 || ^6 + vite: ^5.2.0 || ^6 || ^7 '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@typescript-eslint/eslint-plugin@8.33.0': - resolution: {integrity: sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==} + '@typescript-eslint/eslint-plugin@8.42.0': + resolution: {integrity: sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.33.0 + '@typescript-eslint/parser': ^8.42.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.33.0': - resolution: {integrity: sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==} + '@typescript-eslint/parser@8.42.0': + resolution: {integrity: sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.33.0': - resolution: {integrity: sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==} + '@typescript-eslint/project-service@8.42.0': + resolution: {integrity: sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.33.0': - resolution: {integrity: sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==} + '@typescript-eslint/scope-manager@8.42.0': + resolution: {integrity: sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.33.0': - resolution: {integrity: sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==} + '@typescript-eslint/tsconfig-utils@8.42.0': + resolution: {integrity: sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.33.0': - resolution: {integrity: sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==} + '@typescript-eslint/type-utils@8.42.0': + resolution: {integrity: sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.33.0': - resolution: {integrity: sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==} + '@typescript-eslint/types@8.42.0': + resolution: {integrity: sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.33.0': - resolution: {integrity: sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==} + '@typescript-eslint/typescript-estree@8.42.0': + resolution: {integrity: sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.33.0': - resolution: {integrity: sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==} + '@typescript-eslint/utils@8.42.0': + resolution: {integrity: sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.33.0': - resolution: {integrity: sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==} + '@typescript-eslint/visitor-keys@8.42.0': + resolution: {integrity: sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -692,11 +673,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -723,11 +699,11 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -784,15 +760,6 @@ packages: engines: {node: '>=4'} hasBin: true - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -816,8 +783,8 @@ packages: devalue@5.3.2: resolution: {integrity: sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} esbuild@0.25.9: @@ -829,14 +796,14 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-prettier@10.1.5: - resolution: {integrity: sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true peerDependencies: eslint: '>=7.0.0' - eslint-plugin-svelte@3.11.0: - resolution: {integrity: sha512-KliWlkieHyEa65aQIkRwUFfHzT5Cn4u3BQQsu3KlkJOs7c1u7ryn84EWaOjEzilbKgttT4OfBURA8Uc4JBSQIw==} + eslint-plugin-svelte@3.12.0: + resolution: {integrity: sha512-jUAUTm+6Z7wu/UkpJgzYToa17FSKXesoCDcA26Znsk4oPvMxtnU415kIb0OP9SUIAAVY6jCYngiVBBgvl70i9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 @@ -845,10 +812,6 @@ packages: svelte: optional: true - eslint-scope@8.3.0: - resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -857,16 +820,12 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@4.2.1: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.27.0: - resolution: {integrity: sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==} + eslint@9.34.0: + resolution: {integrity: sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -878,10 +837,6 @@ packages: esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - espree@10.3.0: - resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -890,8 +845,8 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - esrap@1.4.6: - resolution: {integrity: sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==} + esrap@2.1.0: + resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -921,14 +876,6 @@ packages: fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - fdir@6.4.3: - resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -996,8 +943,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.4: - resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} import-fresh@3.3.1: @@ -1008,8 +955,8 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inter-ui@4.1.0: - resolution: {integrity: sha512-lUJ5YLtpj7jWsrhTbN5w32MLOqISGFGOTMRNn+IGhLrbt67z5yvmBRoqpLCWLVlJ+l7kqnH6su4meEqvLMfAAA==} + inter-ui@4.1.1: + resolution: {integrity: sha512-451h0J29HyOmA+JXgSi/6M12tL7ZCZ8arYKZUXiOXTJpJbAKqJvFh3k5SiV3x7tKe0C0KyrKUUiQIvvZ2PQDcA==} engines: {node: '>=16.0.0'} is-extglob@2.1.1: @@ -1030,8 +977,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true js-yaml@4.1.0: @@ -1145,9 +1092,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magic-string@0.30.18: resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==} @@ -1283,11 +1227,13 @@ packages: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier-plugin-tailwindcss@0.6.11: - resolution: {integrity: sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==} + prettier-plugin-tailwindcss@0.6.14: + resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==} engines: {node: '>=14.21.3'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' '@prettier/plugin-pug': '*' '@shopify/prettier-plugin-liquid': '*' '@trivago/prettier-plugin-sort-imports': '*' @@ -1307,6 +1253,10 @@ packages: peerDependenciesMeta: '@ianvs/prettier-plugin-sort-imports': optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true '@prettier/plugin-pug': optional: true '@shopify/prettier-plugin-liquid': @@ -1338,8 +1288,8 @@ packages: prettier-plugin-svelte: optional: true - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} hasBin: true @@ -1362,8 +1312,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.50.1: - resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} + rollup@4.50.0: + resolution: {integrity: sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1390,8 +1340,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - sirv@3.0.2: - resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} engines: {node: '>=18'} source-map-js@1.2.1: @@ -1406,8 +1356,8 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - svelte-check@4.2.1: - resolution: {integrity: sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA==} + svelte-check@4.3.1: + resolution: {integrity: sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: @@ -1423,26 +1373,26 @@ packages: svelte: optional: true - svelte-highlight@7.8.3: - resolution: {integrity: sha512-i4CE/6yda1fCh0ovUVATk1S1feu1y3+CV+l1brgtMPPRO9VTGq+hPpUjVEJWQkE7hPAgwgVpHccoa5M2gpKxYQ==} + svelte-highlight@7.8.4: + resolution: {integrity: sha512-aVp+Q0hH9kI7PlSDrklmFTF4Uj7wYj7UGuqkREnkXlqpEffxr2g6esZcMMaTgECdg5rb2mJqM88+nwS10ecoTg==} - svelte@5.33.4: - resolution: {integrity: sha512-w2+ksGaZRgZgQdP2dtOjlpkdQwX3ftc8+BH/W9XgX6rP1FZHCeuXQnmyeHPFLbG2Gjj9pp2ak8iIeVure+n7IA==} + svelte@5.38.6: + resolution: {integrity: sha512-ltBPlkvqk3bgCK7/N323atUpP3O3Y+DrGV4dcULrsSn4fZaaNnOmdplNznwfdWclAgvSr5rxjtzn/zJhRm6TKg==} engines: {node: '>=18'} - tailwindcss@4.1.7: - resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==} + tailwindcss@4.1.12: + resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==} - tapable@2.2.2: - resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + tapable@2.2.3: + resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} engines: {node: '>=6'} tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} to-regex-range@5.0.1: @@ -1463,15 +1413,15 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.33.0: - resolution: {integrity: sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==} + typescript-eslint@8.42.0: + resolution: {integrity: sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} hasBin: true @@ -1481,8 +1431,8 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vite@6.3.6: - resolution: {integrity: sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==} + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -1521,10 +1471,10 @@ packages: yaml: optional: true - vitefu@1.0.6: - resolution: {integrity: sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==} + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 peerDependenciesMeta: vite: optional: true @@ -1555,11 +1505,6 @@ packages: snapshots: - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - '@esbuild/aix-ppc64@0.25.9': optional: true @@ -1638,41 +1583,36 @@ snapshots: '@esbuild/win32-x64@0.25.9': optional: true - '@eslint-community/eslint-utils@4.5.1(eslint@9.27.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.8.0(eslint@9.34.0(jiti@2.5.1))': dependencies: - eslint: 9.27.0(jiti@2.4.2) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.7.0(eslint@9.27.0(jiti@2.4.2))': - dependencies: - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.34.0(jiti@2.5.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/compat@1.3.0(eslint@9.27.0(jiti@2.4.2))': + '@eslint/compat@1.3.2(eslint@9.34.0(jiti@2.5.1))': optionalDependencies: - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.34.0(jiti@2.5.1) - '@eslint/config-array@0.20.0': + '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.0 + debug: 4.4.1 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.1': {} + '@eslint/config-helpers@0.3.1': {} - '@eslint/core@0.14.0': + '@eslint/core@0.15.2': dependencies: '@types/json-schema': 7.0.15 '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.0 - espree: 10.3.0 + debug: 4.4.1 + espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 @@ -1682,13 +1622,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.27.0': {} + '@eslint/js@9.34.0': {} '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.3.1': + '@eslint/plugin-kit@0.3.5': dependencies: - '@eslint/core': 0.14.0 + '@eslint/core': 0.15.2 levn: 0.4.1 '@fontsource/noto-sans-mono@5.2.7': {} @@ -1704,29 +1644,27 @@ snapshots: '@humanwhocodes/retry@0.3.1': {} - '@humanwhocodes/retry@0.4.2': {} + '@humanwhocodes/retry@0.4.3': {} '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 - '@jridgewell/gen-mapping@0.3.8': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/set-array@1.2.1': {} + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 - '@jridgewell/sourcemap-codec@1.5.4': {} + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.30': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 @@ -1747,88 +1685,84 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@rollup/rollup-android-arm-eabi@4.50.1': + '@rollup/rollup-android-arm-eabi@4.50.0': optional: true - '@rollup/rollup-android-arm64@4.50.1': + '@rollup/rollup-android-arm64@4.50.0': optional: true - '@rollup/rollup-darwin-arm64@4.50.1': + '@rollup/rollup-darwin-arm64@4.50.0': optional: true - '@rollup/rollup-darwin-x64@4.50.1': + '@rollup/rollup-darwin-x64@4.50.0': optional: true - '@rollup/rollup-freebsd-arm64@4.50.1': + '@rollup/rollup-freebsd-arm64@4.50.0': optional: true - '@rollup/rollup-freebsd-x64@4.50.1': + '@rollup/rollup-freebsd-x64@4.50.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + '@rollup/rollup-linux-arm-gnueabihf@4.50.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.50.1': + '@rollup/rollup-linux-arm-musleabihf@4.50.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.50.1': + '@rollup/rollup-linux-arm64-gnu@4.50.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.50.1': + '@rollup/rollup-linux-arm64-musl@4.50.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + '@rollup/rollup-linux-loongarch64-gnu@4.50.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.50.1': + '@rollup/rollup-linux-ppc64-gnu@4.50.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.50.1': + '@rollup/rollup-linux-riscv64-gnu@4.50.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.50.1': + '@rollup/rollup-linux-riscv64-musl@4.50.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.50.1': + '@rollup/rollup-linux-s390x-gnu@4.50.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.50.1': + '@rollup/rollup-linux-x64-gnu@4.50.0': optional: true - '@rollup/rollup-linux-x64-musl@4.50.1': + '@rollup/rollup-linux-x64-musl@4.50.0': optional: true - '@rollup/rollup-openharmony-arm64@4.50.1': + '@rollup/rollup-openharmony-arm64@4.50.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.50.1': + '@rollup/rollup-win32-arm64-msvc@4.50.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.50.1': + '@rollup/rollup-win32-ia32-msvc@4.50.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.50.1': + '@rollup/rollup-win32-x64-msvc@4.50.0': optional: true '@standard-schema/spec@1.0.0': {} - '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1)': - dependencies: - acorn: 8.14.1 - '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.37.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.9(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.37.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.37.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)) - '@sveltejs/kit@2.37.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -1839,222 +1773,219 @@ snapshots: mrmime: 2.0.1 sade: 1.8.1 set-cookie-parser: 2.7.1 - sirv: 3.0.2 - svelte: 5.33.4 - vite: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) + sirv: 3.0.1 + svelte: 5.38.6 + vite: 6.3.5(jiti@2.5.1)(lightningcss@1.30.1) - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)) debug: 4.4.1 - svelte: 5.33.4 - vite: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) + svelte: 5.38.6 + vite: 6.3.5(jiti@2.5.1)(lightningcss@1.30.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)))(svelte@5.38.6)(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)) debug: 4.4.1 deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.17 - svelte: 5.33.4 - vite: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) - vitefu: 1.0.6(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + magic-string: 0.30.18 + svelte: 5.38.6 + vite: 6.3.5(jiti@2.5.1)(lightningcss@1.30.1) + vitefu: 1.1.1(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)) transitivePeerDependencies: - supports-color - '@tailwindcss/forms@0.5.10(tailwindcss@4.1.7)': + '@tailwindcss/forms@0.5.10(tailwindcss@4.1.12)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 4.1.7 + tailwindcss: 4.1.12 - '@tailwindcss/node@4.1.7': + '@tailwindcss/node@4.1.12': dependencies: - '@ampproject/remapping': 2.3.0 - enhanced-resolve: 5.18.1 - jiti: 2.4.2 + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.5.1 lightningcss: 1.30.1 magic-string: 0.30.18 source-map-js: 1.2.1 - tailwindcss: 4.1.7 + tailwindcss: 4.1.12 - '@tailwindcss/oxide-android-arm64@4.1.7': + '@tailwindcss/oxide-android-arm64@4.1.12': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.7': + '@tailwindcss/oxide-darwin-arm64@4.1.12': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.7': + '@tailwindcss/oxide-darwin-x64@4.1.12': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.7': + '@tailwindcss/oxide-freebsd-x64@4.1.12': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.7': + '@tailwindcss/oxide-linux-arm64-musl@4.1.12': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.7': + '@tailwindcss/oxide-linux-x64-gnu@4.1.12': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.7': + '@tailwindcss/oxide-linux-x64-musl@4.1.12': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.7': + '@tailwindcss/oxide-wasm32-wasi@4.1.12': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.7': + '@tailwindcss/oxide-win32-x64-msvc@4.1.12': optional: true - '@tailwindcss/oxide@4.1.7': + '@tailwindcss/oxide@4.1.12': dependencies: detect-libc: 2.0.4 tar: 7.4.3 optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.7 - '@tailwindcss/oxide-darwin-arm64': 4.1.7 - '@tailwindcss/oxide-darwin-x64': 4.1.7 - '@tailwindcss/oxide-freebsd-x64': 4.1.7 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.7 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.7 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.7 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.7 - '@tailwindcss/oxide-linux-x64-musl': 4.1.7 - '@tailwindcss/oxide-wasm32-wasi': 4.1.7 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.7 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.7 - - '@tailwindcss/typography@0.5.16(tailwindcss@4.1.7)': + '@tailwindcss/oxide-android-arm64': 4.1.12 + '@tailwindcss/oxide-darwin-arm64': 4.1.12 + '@tailwindcss/oxide-darwin-x64': 4.1.12 + '@tailwindcss/oxide-freebsd-x64': 4.1.12 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.12 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.12 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.12 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.12 + '@tailwindcss/oxide-linux-x64-musl': 4.1.12 + '@tailwindcss/oxide-wasm32-wasi': 4.1.12 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.12 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.12 + + '@tailwindcss/typography@0.5.16(tailwindcss@4.1.12)': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 4.1.7 + tailwindcss: 4.1.12 - '@tailwindcss/vite@4.1.7(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@tailwindcss/vite@4.1.12(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1))': dependencies: - '@tailwindcss/node': 4.1.7 - '@tailwindcss/oxide': 4.1.7 - tailwindcss: 4.1.7 - vite: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) + '@tailwindcss/node': 4.1.12 + '@tailwindcss/oxide': 4.1.12 + tailwindcss: 4.1.12 + vite: 6.3.5(jiti@2.5.1)(lightningcss@1.30.1) '@types/cookie@0.6.0': {} - '@types/estree@1.0.6': {} - '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} - '@typescript-eslint/eslint-plugin@8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.0 - '@typescript-eslint/type-utils': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.33.0 - eslint: 9.27.0(jiti@2.4.2) + '@typescript-eslint/parser': 8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.42.0 + '@typescript-eslint/type-utils': 8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.42.0 + eslint: 9.34.0(jiti@2.5.1) graphemer: 1.4.0 - ignore: 7.0.4 + ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: - '@typescript-eslint/scope-manager': 8.33.0 - '@typescript-eslint/types': 8.33.0 - '@typescript-eslint/typescript-estree': 8.33.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.33.0 + '@typescript-eslint/scope-manager': 8.42.0 + '@typescript-eslint/types': 8.42.0 + '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.42.0 debug: 4.4.1 - eslint: 9.27.0(jiti@2.4.2) - typescript: 5.8.3 + eslint: 9.34.0(jiti@2.5.1) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.33.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.42.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.33.0(typescript@5.8.3) - '@typescript-eslint/types': 8.33.0 + '@typescript-eslint/tsconfig-utils': 8.42.0(typescript@5.9.2) + '@typescript-eslint/types': 8.42.0 debug: 4.4.1 + typescript: 5.9.2 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/scope-manager@8.33.0': + '@typescript-eslint/scope-manager@8.42.0': dependencies: - '@typescript-eslint/types': 8.33.0 - '@typescript-eslint/visitor-keys': 8.33.0 + '@typescript-eslint/types': 8.42.0 + '@typescript-eslint/visitor-keys': 8.42.0 - '@typescript-eslint/tsconfig-utils@8.33.0(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.42.0(typescript@5.9.2)': dependencies: - typescript: 5.8.3 + typescript: 5.9.2 - '@typescript-eslint/type-utils@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: - '@typescript-eslint/typescript-estree': 8.33.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/types': 8.42.0 + '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) debug: 4.4.1 - eslint: 9.27.0(jiti@2.4.2) - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + eslint: 9.34.0(jiti@2.5.1) + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.33.0': {} + '@typescript-eslint/types@8.42.0': {} - '@typescript-eslint/typescript-estree@8.33.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.42.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/project-service': 8.33.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.33.0(typescript@5.8.3) - '@typescript-eslint/types': 8.33.0 - '@typescript-eslint/visitor-keys': 8.33.0 + '@typescript-eslint/project-service': 8.42.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.42.0(typescript@5.9.2) + '@typescript-eslint/types': 8.42.0 + '@typescript-eslint/visitor-keys': 8.42.0 debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.33.0 - '@typescript-eslint/types': 8.33.0 - '@typescript-eslint/typescript-estree': 8.33.0(typescript@5.8.3) - eslint: 9.27.0(jiti@2.4.2) - typescript: 5.8.3 + '@eslint-community/eslint-utils': 4.8.0(eslint@9.34.0(jiti@2.5.1)) + '@typescript-eslint/scope-manager': 8.42.0 + '@typescript-eslint/types': 8.42.0 + '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) + eslint: 9.34.0(jiti@2.5.1) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.33.0': + '@typescript-eslint/visitor-keys@8.42.0': dependencies: - '@typescript-eslint/types': 8.33.0 + '@typescript-eslint/types': 8.42.0 eslint-visitor-keys: 4.2.1 acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 - acorn@8.14.1: {} - acorn@8.15.0: {} ajv@6.12.6: @@ -2076,12 +2007,12 @@ snapshots: balanced-match@1.0.2: {} - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -2128,10 +2059,6 @@ snapshots: cssesc@3.0.0: {} - debug@4.4.0: - dependencies: - ms: 2.1.3 - debug@4.4.1: dependencies: ms: 2.1.3 @@ -2144,10 +2071,10 @@ snapshots: devalue@5.3.2: {} - enhanced-resolve@5.18.1: + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.2 + tapable: 2.2.3 esbuild@0.25.9: optionalDependencies: @@ -2180,15 +2107,15 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.5(eslint@9.27.0(jiti@2.4.2)): + eslint-config-prettier@10.1.8(eslint@9.34.0(jiti@2.5.1)): dependencies: - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.34.0(jiti@2.5.1) - eslint-plugin-svelte@3.11.0(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4): + eslint-plugin-svelte@3.12.0(eslint@9.34.0(jiti@2.5.1))(svelte@5.38.6): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) - '@jridgewell/sourcemap-codec': 1.5.4 - eslint: 9.27.0(jiti@2.4.2) + '@eslint-community/eslint-utils': 4.8.0(eslint@9.34.0(jiti@2.5.1)) + '@jridgewell/sourcemap-codec': 1.5.5 + eslint: 9.34.0(jiti@2.5.1) esutils: 2.0.3 globals: 16.3.0 known-css-properties: 0.37.0 @@ -2196,17 +2123,12 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) semver: 7.7.2 - svelte-eslint-parser: 1.3.1(svelte@5.33.4) + svelte-eslint-parser: 1.3.1(svelte@5.38.6) optionalDependencies: - svelte: 5.33.4 + svelte: 5.38.6 transitivePeerDependencies: - ts-node - eslint-scope@8.3.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -2214,33 +2136,31 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.0: {} - eslint-visitor-keys@4.2.1: {} - eslint@9.27.0(jiti@2.4.2): + eslint@9.34.0(jiti@2.5.1): dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.27.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.8.0(eslint@9.34.0(jiti@2.5.1)) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.1 - '@eslint/core': 0.14.0 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.27.0 - '@eslint/plugin-kit': 0.3.1 + '@eslint/js': 9.34.0 + '@eslint/plugin-kit': 0.3.5 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.2 - '@types/estree': 1.0.6 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.1 escape-string-regexp: 4.0.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -2256,18 +2176,12 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.4.2 + jiti: 2.5.1 transitivePeerDependencies: - supports-color esm-env@1.2.2: {} - espree@10.3.0: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 4.2.0 - espree@10.4.0: dependencies: acorn: 8.15.0 @@ -2278,9 +2192,9 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@1.4.6: + esrap@2.1.0: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 esrecurse@4.3.0: dependencies: @@ -2308,10 +2222,6 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.4.3(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -2361,7 +2271,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.4: {} + ignore@7.0.5: {} import-fresh@3.3.1: dependencies: @@ -2370,7 +2280,7 @@ snapshots: imurmurhash@0.1.4: {} - inter-ui@4.1.0: {} + inter-ui@4.1.1: {} is-extglob@2.1.1: {} @@ -2382,11 +2292,11 @@ snapshots: is-reference@3.0.3: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 isexe@2.0.0: {} - jiti@2.4.2: {} + jiti@2.5.1: {} js-yaml@4.1.0: dependencies: @@ -2470,10 +2380,6 @@ snapshots: lodash.merge@4.6.2: {} - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.18: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2489,11 +2395,11 @@ snapshots: minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minipass@7.1.2: {} @@ -2577,18 +2483,18 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.4.0(prettier@3.5.3)(svelte@5.33.4): + prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.38.6): dependencies: - prettier: 3.5.3 - svelte: 5.33.4 + prettier: 3.6.2 + svelte: 5.38.6 - prettier-plugin-tailwindcss@0.6.11(prettier-plugin-svelte@3.4.0(prettier@3.5.3)(svelte@5.33.4))(prettier@3.5.3): + prettier-plugin-tailwindcss@0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.38.6))(prettier@3.6.2): dependencies: - prettier: 3.5.3 + prettier: 3.6.2 optionalDependencies: - prettier-plugin-svelte: 3.4.0(prettier@3.5.3)(svelte@5.33.4) + prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@5.38.6) - prettier@3.5.3: {} + prettier@3.6.2: {} punycode@2.3.1: {} @@ -2600,31 +2506,31 @@ snapshots: reusify@1.1.0: {} - rollup@4.50.1: + rollup@4.50.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.50.1 - '@rollup/rollup-android-arm64': 4.50.1 - '@rollup/rollup-darwin-arm64': 4.50.1 - '@rollup/rollup-darwin-x64': 4.50.1 - '@rollup/rollup-freebsd-arm64': 4.50.1 - '@rollup/rollup-freebsd-x64': 4.50.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.50.1 - '@rollup/rollup-linux-arm-musleabihf': 4.50.1 - '@rollup/rollup-linux-arm64-gnu': 4.50.1 - '@rollup/rollup-linux-arm64-musl': 4.50.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.50.1 - '@rollup/rollup-linux-ppc64-gnu': 4.50.1 - '@rollup/rollup-linux-riscv64-gnu': 4.50.1 - '@rollup/rollup-linux-riscv64-musl': 4.50.1 - '@rollup/rollup-linux-s390x-gnu': 4.50.1 - '@rollup/rollup-linux-x64-gnu': 4.50.1 - '@rollup/rollup-linux-x64-musl': 4.50.1 - '@rollup/rollup-openharmony-arm64': 4.50.1 - '@rollup/rollup-win32-arm64-msvc': 4.50.1 - '@rollup/rollup-win32-ia32-msvc': 4.50.1 - '@rollup/rollup-win32-x64-msvc': 4.50.1 + '@rollup/rollup-android-arm-eabi': 4.50.0 + '@rollup/rollup-android-arm64': 4.50.0 + '@rollup/rollup-darwin-arm64': 4.50.0 + '@rollup/rollup-darwin-x64': 4.50.0 + '@rollup/rollup-freebsd-arm64': 4.50.0 + '@rollup/rollup-freebsd-x64': 4.50.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.0 + '@rollup/rollup-linux-arm-musleabihf': 4.50.0 + '@rollup/rollup-linux-arm64-gnu': 4.50.0 + '@rollup/rollup-linux-arm64-musl': 4.50.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.50.0 + '@rollup/rollup-linux-ppc64-gnu': 4.50.0 + '@rollup/rollup-linux-riscv64-gnu': 4.50.0 + '@rollup/rollup-linux-riscv64-musl': 4.50.0 + '@rollup/rollup-linux-s390x-gnu': 4.50.0 + '@rollup/rollup-linux-x64-gnu': 4.50.0 + '@rollup/rollup-linux-x64-musl': 4.50.0 + '@rollup/rollup-openharmony-arm64': 4.50.0 + '@rollup/rollup-win32-arm64-msvc': 4.50.0 + '@rollup/rollup-win32-ia32-msvc': 4.50.0 + '@rollup/rollup-win32-x64-msvc': 4.50.0 fsevents: 2.3.3 run-parallel@1.2.0: @@ -2645,7 +2551,7 @@ snapshots: shebang-regex@3.0.0: {} - sirv@3.0.2: + sirv@3.0.1: dependencies: '@polka/url': 1.0.0-next.29 mrmime: 2.0.1 @@ -2659,19 +2565,19 @@ snapshots: dependencies: has-flag: 4.0.0 - svelte-check@4.2.1(picomatch@4.0.3)(svelte@5.33.4)(typescript@5.8.3): + svelte-check@4.3.1(picomatch@4.0.3)(svelte@5.38.6)(typescript@5.9.2): dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.30 chokidar: 4.0.3 - fdir: 6.4.3(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.33.4 - typescript: 5.8.3 + svelte: 5.38.6 + typescript: 5.9.2 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.3.1(svelte@5.33.4): + svelte-eslint-parser@1.3.1(svelte@5.38.6): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -2680,32 +2586,32 @@ snapshots: postcss-scss: 4.0.9(postcss@8.5.6) postcss-selector-parser: 7.1.0 optionalDependencies: - svelte: 5.33.4 + svelte: 5.38.6 - svelte-highlight@7.8.3: + svelte-highlight@7.8.4: dependencies: highlight.js: 11.11.1 - svelte@5.33.4: + svelte@5.38.6: dependencies: - '@ampproject/remapping': 2.3.0 - '@jridgewell/sourcemap-codec': 1.5.0 - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.1) - '@types/estree': 1.0.6 - acorn: 8.14.1 + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 esm-env: 1.2.2 - esrap: 1.4.6 + esrap: 2.1.0 is-reference: 3.0.3 locate-character: 3.0.0 - magic-string: 0.30.17 + magic-string: 0.30.18 zimmerframe: 1.1.2 - tailwindcss@4.1.7: {} + tailwindcss@4.1.12: {} - tapable@2.2.2: {} + tapable@2.2.3: {} tar@7.4.3: dependencies: @@ -2716,7 +2622,7 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 - tinyglobby@0.2.15: + tinyglobby@0.2.14: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 @@ -2727,25 +2633,26 @@ snapshots: totalist@3.0.1: {} - ts-api-utils@2.1.0(typescript@5.8.3): + ts-api-utils@2.1.0(typescript@5.9.2): dependencies: - typescript: 5.8.3 + typescript: 5.9.2 type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.27.0(jiti@2.4.2) - typescript: 5.8.3 + '@typescript-eslint/eslint-plugin': 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.5.1) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - typescript@5.8.3: {} + typescript@5.9.2: {} uri-js@4.4.1: dependencies: @@ -2753,22 +2660,22 @@ snapshots: util-deprecate@1.0.2: {} - vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1): + vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.50.1 - tinyglobby: 0.2.15 + rollup: 4.50.0 + tinyglobby: 0.2.14 optionalDependencies: fsevents: 2.3.3 - jiti: 2.4.2 + jiti: 2.5.1 lightningcss: 1.30.1 - vitefu@1.0.6(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)): + vitefu@1.1.1(vite@6.3.5(jiti@2.5.1)(lightningcss@1.30.1)): optionalDependencies: - vite: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) + vite: 6.3.5(jiti@2.5.1)(lightningcss@1.30.1) which@2.0.2: dependencies: diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/ChatInterface.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/ChatInterface.svelte new file mode 100644 index 00000000000..7cbdb47ada1 --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/ChatInterface.svelte @@ -0,0 +1,216 @@ + + +
+ +
+ +
+
+
+ + + +
+
+

AI Assistant

+

Analyzing {selectedComponent.name}

+
+
+ +
+ + +
+ {#each messages as message} +
+
+
+ +
+ {#if message.role === 'user'} + + + + {:else} + + + + {/if} +
+ + +
+
+
{message.content}
+
+ +
+ {new Intl.DateTimeFormat('de-DE', { + hour: '2-digit', + minute: '2-digit', + day: '2-digit', + month: '2-digit' + }).format(message.timestamp)} +
+
+
+
+
+ {/each} + + {#if isLoading || streamingContent} +
+
+
+
+ + + +
+ + +
+ {#if streamingContent} +
+ {streamingContent}ā–Š +
+ {:else} +
+
+
+
+
+
+
+ {/if} +
+
+
+
+ {/if} +
+ + +
+
+ + +
+
+
+ + +
+ +
+ +
+ + +
+ {#if selectedUnit} + + {:else} +
+
+ + + +

No file selected

+

+ Select a translation unit from the sidebar to view its contents +

+
+
+ {/if} +
+
+
diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/WelcomeScreen.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/WelcomeScreen.svelte new file mode 100644 index 00000000000..8297dfd1184 --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/WelcomeScreen.svelte @@ -0,0 +1,124 @@ + + +
+ +
+
+ šŸ‘‹ +

+ Hi, I'm your CodAIze Assistant +
+
+ + + +
+
+

+
+ +

+ I help you find security vulnerabilities, understand data flows, and analyze code dependencies using graph-based analysis. +

+
+ + + {#if components.length > 0} +
+
+

Choose a component to analyze

+
+ {#each components as component} + + {/each} +
+
+
+ {/if} + + +
+

Popular CPG analysis queries:

+
+ {#each suggestedQuestions as question} + + {/each} +
+
+ + +
+ e.key === 'Enter' && handleSendMessage()} + /> + +
+
diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/index.ts b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/index.ts new file mode 100644 index 00000000000..411a999e4d8 --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/index.ts @@ -0,0 +1,3 @@ +export { default as WelcomeScreen } from './WelcomeScreen.svelte'; +export { default as ChatInterface } from './ChatInterface.svelte'; + diff --git a/codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte b/codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte new file mode 100644 index 00000000000..56f54c5b880 --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte @@ -0,0 +1,126 @@ + + +
+ +
+
+
{translationUnit.name}
+
+ +
+
+ + + +
+ + +
+
+ + +
+
+ +
+ +
+ console.log('Node clicked:', node)} + /> +
+
+
\ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/analysis/FileTree.svelte b/codyze-console/src/main/webapp/src/lib/components/analysis/FileTree.svelte new file mode 100644 index 00000000000..8c92aa9d73e --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/analysis/FileTree.svelte @@ -0,0 +1,162 @@ + + +
+

Files

+ + {#snippet TreeNode(nodes: TreeNode[], depth = 0)} +
    + {#each nodes as node} +
  • + {#if node.type === 'folder'} + + + + {#if expandedFolders.has(node.path) || node.expanded} + {@render TreeNode(node.children, depth + 1)} + {/if} + {:else} + + + {/if} +
  • + {/each} +
+ {/snippet} + + {@render TreeNode(fileTree)} +
\ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/analysis/index.ts b/codyze-console/src/main/webapp/src/lib/components/analysis/index.ts index 7aa5c441f27..7e25f122834 100644 --- a/codyze-console/src/main/webapp/src/lib/components/analysis/index.ts +++ b/codyze-console/src/main/webapp/src/lib/components/analysis/index.ts @@ -1,5 +1,7 @@ // Analysis components export { default as AnalysisResult } from './AnalysisResult.svelte'; +export { default as CodeViewer } from './CodeViewer.svelte'; +export { default as FileTree } from './FileTree.svelte'; export { default as Finding } from './Finding.svelte'; export { default as FindingOverlay } from './FindingOverlay.svelte'; export { default as FindingsList } from './FindingsList.svelte'; diff --git a/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte b/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte index a81a7b35729..26ea46ed841 100644 --- a/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte @@ -19,7 +19,8 @@ const navItems = [ { name: 'Dashboard', path: '/dashboard', icon: 'home' }, { name: 'Requirements', path: '/requirements', icon: 'clipboard-check' }, - { name: 'Components', path: '/components', icon: 'code' } + { name: 'Components', path: '/components', icon: 'code' }, + { name: 'AI Assistant', path: '/ai-assistant', icon: 'sparkles' } ]; let currentPath = $derived($page.url.pathname); @@ -81,6 +82,12 @@ stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /> + {:else if item.icon === 'sparkles'} + {/if} {item.name} diff --git a/codyze-console/src/main/webapp/src/lib/services/apiService.ts b/codyze-console/src/main/webapp/src/lib/services/apiService.ts new file mode 100644 index 00000000000..52d34120d62 --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/services/apiService.ts @@ -0,0 +1,122 @@ +interface ApiResponse { + ok: boolean; + status: number; + statusText: string; + data?: T; + error?: string; +} + +class ApiService { + constructor(private readonly baseUrl: string = "") { + } + + private async request( + url: string, + options: RequestInit + ): Promise> { + try { + const response = await fetch(`${this.baseUrl}${url}`, { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + ...(options.headers ?? {}) + }, + ...options + }); + + const data = response.ok ? await response.json() : undefined; + + return { + ok: response.ok, + status: response.status, + statusText: response.statusText, + data, + error: response.ok ? undefined : `HTTP ${response.status}: ${response.statusText}` + }; + } catch (err) { + return { + ok: false, + status: 0, + statusText: "Network Error", + error: err instanceof Error ? err.message : String(err) + }; + } + } + + get(url: string, headers: Record = {}) { + return this.request(url, {method: "GET", headers}); + } + + post(url: string, body: any, headers: Record = {}) { + return this.request(url, { + method: "POST", + headers, + body: JSON.stringify(body) + }); + } + + // TODO: Refactor this with the new post api + async streamPost(url: string, body: any, onChunk: (chunk: string) => void, onError?: (error: string) => void, onComplete?: () => void): Promise { + try { + const response = await fetch(`${this.baseUrl}${url}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'text/event-stream', + 'Cache-Control': 'no-cache' + }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + if (onError) { + onError(`HTTP ${response.status}: ${response.statusText}`); + } + return; + } + + const reader = response.body?.getReader(); + if (!reader) { + if (onError) { + onError('Failed to get response reader'); + } + return; + } + + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + if (onComplete) { + onComplete(); + } + break; + } + + buffer += decoder.decode(value, { stream: true }); + + // Process SSE data + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // Keep incomplete line in buffer + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); // Remove 'data: ' prefix + if (data.trim()) { + onChunk(data); + } + } + } + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : String(error)); + } + } + } +} + +export default ApiService; \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts b/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts new file mode 100644 index 00000000000..ff94b0714fa --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts @@ -0,0 +1,30 @@ +import ApiService from './apiService'; + +export interface LLMMessage { + role: 'user' | 'assistant' | 'system'; + content: string; +} + +export interface StreamingCallbacks { + onChunk: (chunk: string) => void; + onError?: (error: string) => void; + onComplete?: () => void; +} + +class LLMAgent { + private apiService = new ApiService(); + + async chat(messages: LLMMessage[], callbacks: StreamingCallbacks): Promise { + const requestData = { + messages: messages.map(msg => ({ + role: msg.role, + content: msg.content + })) + }; + + await this.apiService.streamPost('/api/chat', requestData, callbacks.onChunk, callbacks.onError, callbacks.onComplete); + } +} + +export const llmAgent = new LLMAgent(); + diff --git a/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.svelte b/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.svelte new file mode 100644 index 00000000000..a6e72bd4cc1 --- /dev/null +++ b/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.svelte @@ -0,0 +1,157 @@ + + +
+ + +
+ {#if hasError} +
+ + + +

No components available

+

+ Could not load analysis components. Please ensure your project has been analyzed and the backend service is running. +

+
+ {:else} + {#if showWelcome} + + {:else if selectedComponent} + currentMessage = message} + /> + {/if} + {/if} +
+
diff --git a/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.ts b/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.ts new file mode 100644 index 00000000000..82b73e3a9eb --- /dev/null +++ b/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.ts @@ -0,0 +1,16 @@ +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ fetch }) => { + try { + const response = await fetch('/api/result'); + const result = await response.json(); + return { + components: result.components || [] + }; + } catch (error) { + console.error('Failed to load components:', error); + return { + components: null + }; + } +}; diff --git a/codyze-console/src/main/webapp/vite.config.ts b/codyze-console/src/main/webapp/vite.config.ts index 2eaee2d76e4..fb52cb9104f 100644 --- a/codyze-console/src/main/webapp/vite.config.ts +++ b/codyze-console/src/main/webapp/vite.config.ts @@ -6,7 +6,12 @@ export default defineConfig({ plugins: [tailwindcss(), sveltekit()], server: { proxy: { - '/api': 'http://localhost:8080' + '/api': 'http://localhost:8080', + '/ollama': { + target: 'http://localhost:11434', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/ollama/, '') + } } } }); diff --git a/cpg-mcp/build.gradle.kts b/cpg-mcp/build.gradle.kts index 2ae2d97a276..cf9b13ca714 100644 --- a/cpg-mcp/build.gradle.kts +++ b/cpg-mcp/build.gradle.kts @@ -66,13 +66,14 @@ mavenPublishing { dependencies { implementation(libs.mcp) implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.ktor.server.cors) + implementation(libs.ktor.server.content.negotiation) // Test dependencies testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") // Command line interface support - api(libs.picocli) - annotationProcessor(libs.picocli.codegen) + implementation(libs.clikt) integrationTestImplementation(libs.kotlin.reflect) diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt index 1e1c471cd5d..2311406ad03 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt @@ -25,9 +25,19 @@ */ package de.fraunhofer.aisec.cpg.mcp +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.main +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.int import de.fraunhofer.aisec.cpg.mcp.mcpserver.configureServer -import io.ktor.server.cio.CIO -import io.ktor.server.engine.embeddedServer +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.cio.* +import io.ktor.server.engine.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.routing.* import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport import io.modelcontextprotocol.kotlin.sdk.server.mcp @@ -36,23 +46,29 @@ import kotlinx.coroutines.runBlocking import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered -import picocli.CommandLine -@CommandLine.Option( - names = ["--sse"], - description = - [ - "Provide the port to run SSE (Server Sent Events). If not specified, the MCP server will run using stdio." - ], -) -var ssePort: Int? = null +fun main(args: Array) { + McpServer().main(args) +} + +class McpServer : CliktCommand(name = "mcp-server") { + private val ssePort by + option( + "--sse", + help = + "Port to run SSE (Server Sent Events). If nothing provided, it will use stdio.", + ) + .int() -fun main() { - val port = ssePort - if (port == null) { - runMcpServerUsingStdio() - } else { - runSseMcpServerUsingKtorPlugin(port, configureServer()) + override fun run() { + val port = ssePort + if (port != null) { + println("Starting MCP server in SSE mode on port $port...") + runSseMcpServerUsingKtorPlugin(port, configureServer()) + } else { + println("Starting MCP server in stdio mode...") + runMcpServerUsingStdio() + } } } diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt index 5fdfb24c254..1c6c4cc09d9 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt @@ -25,19 +25,8 @@ */ package de.fraunhofer.aisec.cpg.mcp.mcpserver -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.addCpgAnalyzeTool -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.addCpgApplyConceptsTool -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.addCpgDataflowTool -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.addCpgLlmAnalyzeTool -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.getAllArgs -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.getArgByIndexOrName -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.listAvailableConcepts -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.listAvailableOperations -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.listCalls -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.listCallsTo -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.listConceptsAndOperations -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.listFunctions -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.listRecords +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.* +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.RegisteredTool import io.modelcontextprotocol.kotlin.sdk.Implementation import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.server.Server @@ -74,6 +63,10 @@ fun configureServer(): Server { return server } +fun Server.listTools(): List { + return this.tools.map { (name, registeredTool) -> RegisteredTool(name, registeredTool.tool) } +} + const val cpgDescription = """ This server provides tools to analyze the Fraunhofer AISEC CPG (Code Property Graph) and diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt index ce598439abc..29022dce310 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt @@ -33,7 +33,9 @@ import de.fraunhofer.aisec.cpg.graph.declarations.ParameterDeclaration import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression import de.fraunhofer.aisec.cpg.graph.types.Type +import io.modelcontextprotocol.kotlin.sdk.Tool import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement @Serializable data class NodeInfo( @@ -236,3 +238,31 @@ data class QueryTreeNode( val node: NodeInfo?, val children: List = emptyList(), ) + +@Serializable +data class RegisteredTool(val type: String = "function", val function: ToolFunction) { + constructor( + name: String, + tool: Tool, + ) : this( + function = + ToolFunction( + name = name, + description = tool.description?.replace("\n", "\\n")?.replace("\r", "\\r"), + parameters = ToolFunction.ToolParameters(properties = tool.inputSchema.properties), + ) + ) + + @Serializable + data class ToolFunction( + val name: String, + val description: String?, + val parameters: ToolParameters = ToolParameters(), + ) { + @Serializable + data class ToolParameters( + val type: String = "object", + val properties: Map = emptyMap(), + ) + } +} From 2d42e1bc7cc44bc90a15ee2bbf5d1f6b923274c6 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 11 Sep 2025 10:26:10 +0200 Subject: [PATCH 02/93] Format --- .../kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt index 9a269d0d45f..88137ae0601 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt @@ -187,8 +187,7 @@ class ChatService { // Handle tool calls val toolCalls = message?.tool_calls if (!toolCalls.isNullOrEmpty()) { - emit("\n\nšŸ”§ **Tools Called:**\n") - + emit("\n\n**Tools Called:**\n") for (toolCall in toolCalls) { emit("- **${toolCall.function.name}**\n") val result = executeToolCall(toolCall) From 715219c0658be4cf8536f7a1759636cfb4efc0f0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:55:42 +0200 Subject: [PATCH 03/93] Update dependency io.ktor.plugin to v3.3.0 (#2412) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 96a6bf25f38..d0cc1f5d65e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ slf4j = "2.0.16" clikt = "5.0.2" kaml = "0.94.0" sarif4k = "0.6.0" -ktor = "3.2.1" +ktor = "3.3.0" node = "22.14.0" deno-plugin = "0.1.5" From 7722707f85ac56216a5aee7898a61d56fcfcef95 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:01:05 +0200 Subject: [PATCH 04/93] Update dependency com.charleskorn.kaml:kaml to v0.96.0 (#2430) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d0cc1f5d65e..6c331bd5323 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ publish = "0.33.0" sootup = "2.0.0" slf4j = "2.0.16" clikt = "5.0.2" -kaml = "0.94.0" +kaml = "0.96.0" sarif4k = "0.6.0" ktor = "3.3.0" node = "22.14.0" From 308495f5eb4b49ee036ff72713b1c9e39adc1467 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:18:11 +0200 Subject: [PATCH 05/93] Update dependency @sveltejs/kit to v2.39.1 (#2429) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 740e17d405a..b78c1e14c19 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -23,10 +23,10 @@ importers: version: 5.2.7 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.8(@sveltejs/kit@2.37.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) + version: 3.0.8(@sveltejs/kit@2.39.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) '@sveltejs/kit': specifier: ^2.20.6 - version: 2.37.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 2.39.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 version: 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) @@ -490,8 +490,8 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.37.1': - resolution: {integrity: sha512-4T9rF2Roe7RGvHfcn6+n92Yc2NF88k7ljFz9+wE0jWxyencqRpadr2/CvlcQbbTXf1ozmFxgMO6af+qm+1mPFw==} + '@sveltejs/kit@2.39.1': + resolution: {integrity: sha512-NdgBGHcf/3tXYzPRyQuvsmjI5d3Qp6uhgmlN3uurhyEMN0hMFhdUG83zmWBH8u/QXj6VBmPrKvUn0QXf+Q3/lQ==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -1148,8 +1148,8 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magic-string@0.30.18: - resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==} + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} @@ -1820,11 +1820,11 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.37.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.39.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.37.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.39.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) - '@sveltejs/kit@2.37.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/kit@2.39.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) @@ -1835,7 +1835,7 @@ snapshots: devalue: 5.3.2 esm-env: 1.2.2 kleur: 4.1.5 - magic-string: 0.30.18 + magic-string: 0.30.19 mrmime: 2.0.1 sade: 1.8.1 set-cookie-parser: 2.7.1 @@ -1876,7 +1876,7 @@ snapshots: enhanced-resolve: 5.18.1 jiti: 2.4.2 lightningcss: 1.30.1 - magic-string: 0.30.18 + magic-string: 0.30.19 source-map-js: 1.2.1 tailwindcss: 4.1.7 @@ -2474,7 +2474,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.18: + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 From b8c202ca83cac2d141754e711865daf92b17f112 Mon Sep 17 00:00:00 2001 From: Konrad Weiss Date: Wed, 17 Sep 2025 11:11:03 +0200 Subject: [PATCH 06/93] Update CODEOWNERS (#2425) * Update CODEOWNERS The Code owners are updated with specific responsibilities for passes. * Add Second responsible to language frontends and remove the language specific pass responsibles --- .github/CODEOWNERS | 74 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4dae62f6ce0..b514203e1ce 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,35 +1,77 @@ +##### Here start the pass specific ownerships, new passes should be added here ##### + +# Core Semantic Passes + +*/EvaluationOrderGraphPass*.kt @konradweiss +*/SymbolResolver*.kt @oxisto @konradweiss +*/DFGPass*.kt @KuechA @konradweiss @oxisto +*/ControlFlowSensitiveDFGPass*.kt @KuechA @konradweiss @oxisto +*/ControlDependenceGraphPass*.kt @KuechA @konradweiss @oxisto +*/ProgramDependenceGraphPass*.kt @KuechA @konradweiss @oxisto + +# Resolvers + +*/ImportResolver*.kt @oxisto @konradweiss @maximiliankaul +*/TypeResolver*.kt @oxisto @konradweiss +*/TypeHierarchyResolver*.kt @oxisto @konradweiss +*/DynamicInvokeResolver*.kt @oxisto @peckto +*/ResolveCallExpressionAmbiguityPass*.kt @oxisto @peckto +*/ResolveMemberExpressionAmbiguityPass*.kt @oxisto @peckto + +# Extra Passes + +*/UnreachableEOGPass*.kt @KuechA @konradweiss +*/PrepareSerialization*.kt @peckto @konradweiss + +# Concepts Passes + +*/PythonTempFilePass*.kt @maximiliankaul @lshala +*/PythonFileJoinPass*.kt @maximiliankaul @lshala +*/PythonFileConceptPass*.kt @maximiliankaul @lshala +*/PythonLoggingConceptPass*.kt @maximiliankaul @lshala +*/PythonStdLibConfigurationPass*.kt @oxisto @KuechA +*/TagOverlaysPass*.kt @KuechA @lshala @oxisto +*/LoadPersistedConcepts*.kt @maximiliankaul @lshala +*/ProvideConfigPass*.kt @lshala @oxisto +*/IniFileConfigurationSourcePass*.kt @lshala @oxisto +*/CXXEntryPointsPass*.kt @maximiliankaul @oxisto @KuechA +*/CXXDynamicLoadingPass*.kt @oxisto @KuechA + + +##### Here start the language and module based ownership definitions ##### + * @konradweiss @oxisto @KuechA -*.go @oxisto -cpg-language-go @oxisto +*.go @oxisto @fwendland +cpg-language-go @oxisto @fwendland -*.ts @oxisto -cpg-language-typescript @oxisto +*.ts @oxisto @konradweiss +cpg-language-typescript @oxisto @konradweiss *.py @maximiliankaul @lshala cpg-language-python @maximiliankaul @lshala -*.c @peckto -*.cpp @peckto -cpg-language-cxx @peckto +*.c @peckto @fwendland +*.cpp @peckto @fwendland +cpg-language-cxx @peckto @fwendland -*.ll @KuechA -cpg-language-llvm @KuechA -cpg-analysis @KuechA +*.ll @KuechA @oxisto +cpg-language-llvm @KuechA @oxisto +cpg-analysis @KuechA @oxisto -*.java @konradweiss -cpg-language-java @konradweiss +*.java @konradweiss @oxisto +cpg-language-java @konradweiss @oxisto -cpg-language-jvm @oxisto +cpg-language-jvm @oxisto @konradweiss -cpg-language-ruby @oxisto +cpg-language-ruby @oxisto @konradweiss -cpg-neo4j @peckto +cpg-neo4j @peckto @konradweiss build.gradle.kts @oxisto .github @oxisto -cpg-language-ini @maximiliankaul +cpg-language-ini @maximiliankaul @konradweiss cpg-concepts @maximiliankaul From d9ac76f8f921946584c5785cbe6e698276f7cbb7 Mon Sep 17 00:00:00 2001 From: Leutrim Shala <83644358+lshala@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:34:57 +0200 Subject: [PATCH 07/93] Dokka v2 (#2413) * Change dokka config to new dsl * Delete unused code * Delete .kotlin/sessions directory * Add V2Enabled mode to gradle.properties * Delete tmp.json * Remove set previous docs folder --------- Co-authored-by: Konrad Weiss --- build.gradle.kts | 53 +++++++------------ .../kotlin/cpg.common-conventions.gradle.kts | 9 ---- .../cpg.publishing-conventions.gradle.kts | 2 +- gradle.properties.example | 6 ++- 4 files changed, 26 insertions(+), 44 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index c2f4d7a162d..7108204be17 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,42 +40,29 @@ repositories { mavenCentral() } -allprojects { - plugins.apply("org.jetbrains.dokka") +group = "de.fraunhofer.aisec" - group = "de.fraunhofer.aisec" - - val dokkaPlugin by configurations - dependencies { - dokkaPlugin("org.jetbrains.dokka:versioning-plugin:2.0.0") +// Configure Dokka for the multi-module cpg project +dokka { + val tag = when (val configuredVersion = project.version.toString()) { + "", "unspecified" -> "main" + else -> configuredVersion } -} - -// configure dokka for the multi-module cpg project -// this works together with the dokka configuration in the common-conventions plugin -tasks.dokkaHtmlMultiModule { - val configuredVersion = project.version.toString() - if(configuredVersion.isNotEmpty() && configuredVersion != "unspecified") { - generateDokkaWithVersionTag(this, configuredVersion) - } else { - generateDokkaWithVersionTag(this, "main") + dokkaPublications.html { + outputDirectory.set(layout.buildDirectory.dir("dokkaCustomMultiModuleOutput/$tag")) + + // Collect documentation from all subprojects + subprojects.forEach { + dependencies { + dokka(project(":${it.name}")) + } + } + } + pluginsConfiguration { + versioning { + version.set(tag) + } } -} - -/** - * Takes the old dokka sites in build/dokkaCustomMultiModuleOutput/versions and generates a new site. - * This new site contains the old ones, so copying the newly generated site to the gh page is enough. - * Currently, the mkdocs plugin expects it in docs/dokka/latest. The tags in the dropdown will be - * named based on what we configured here. - */ -fun generateDokkaWithVersionTag(dokkaMultiModuleTask: org.jetbrains.dokka.gradle.AbstractDokkaParentTask, tag: String) { - val oldOutputPath = projectDir.resolve("previousDocs") - val id = "org.jetbrains.dokka.versioning.VersioningPlugin" - val config = """{ "version": "$tag", "olderVersionsDir":"${oldOutputPath.path}" }""" - val mapOf = mapOf(id to config) - - dokkaMultiModuleTask.outputDirectory.set(file(layout.buildDirectory.asFile.get().resolve("dokkaCustomMultiModuleOutput").resolve(tag))) - dokkaMultiModuleTask.pluginsMapConfiguration.set(mapOf) } dependencies { diff --git a/buildSrc/src/main/kotlin/cpg.common-conventions.gradle.kts b/buildSrc/src/main/kotlin/cpg.common-conventions.gradle.kts index d4896ae8515..0d8e5c7453e 100644 --- a/buildSrc/src/main/kotlin/cpg.common-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/cpg.common-conventions.gradle.kts @@ -1,6 +1,5 @@ import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.api.services.BuildServiceParameters.None -import org.jetbrains.dokka.gradle.DokkaTask import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -12,7 +11,6 @@ plugins { signing kotlin("jvm") kotlin("plugin.serialization") - id("org.jetbrains.dokka") } java { @@ -45,13 +43,6 @@ tasks.withType { enabled = false } -val dokkaHtml by tasks.getting(DokkaTask::class) -val javadocJar by tasks.registering(Jar::class) { - dependsOn(dokkaHtml) - archiveClassifier.set("javadoc") - from(dokkaHtml.outputDirectory) -} - // // common compilation configuration // diff --git a/buildSrc/src/main/kotlin/cpg.publishing-conventions.gradle.kts b/buildSrc/src/main/kotlin/cpg.publishing-conventions.gradle.kts index 04ce71127cf..41306e04174 100644 --- a/buildSrc/src/main/kotlin/cpg.publishing-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/cpg.publishing-conventions.gradle.kts @@ -23,7 +23,7 @@ publishing { // Only include javadoc if the includeJavadoc property is set to true (required for maven central) val includeJavadoc: String? by project val javadocJar = if(includeJavadoc.toBoolean()) { - JavadocJar.Dokka("dokkaHtml") + JavadocJar.Dokka("dokkaGenerateModuleHtml") } else { JavadocJar.Empty() } diff --git a/gradle.properties.example b/gradle.properties.example index 55f76e4b8c2..96c60ee7c16 100644 --- a/gradle.properties.example +++ b/gradle.properties.example @@ -9,4 +9,8 @@ enableLLVMFrontend=true enableTypeScriptFrontend=true enableRubyFrontend=true enableJVMFrontend=true -enableINIFrontend=true \ No newline at end of file +enableINIFrontend=true + +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +## Suppress warnings about using the experimental Dokka plugin +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true \ No newline at end of file From a5a17999a7176eadcd10b38263e63b100803e967 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 07:46:58 +0200 Subject: [PATCH 08/93] Update dependency eslint-plugin-svelte to v3.12.4 (#2433) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index b78c1e14c19..16883cd0693 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -50,7 +50,7 @@ importers: version: 10.1.5(eslint@9.27.0(jiti@2.4.2)) eslint-plugin-svelte: specifier: ^3.0.0 - version: 3.11.0(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4) + version: 3.12.4(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4) globals: specifier: ^16.0.0 version: 16.3.0 @@ -262,6 +262,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -345,9 +351,6 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/sourcemap-codec@1.5.4': - resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -835,8 +838,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-svelte@3.11.0: - resolution: {integrity: sha512-KliWlkieHyEa65aQIkRwUFfHzT5Cn4u3BQQsu3KlkJOs7c1u7ryn84EWaOjEzilbKgttT4OfBURA8Uc4JBSQIw==} + eslint-plugin-svelte@3.12.4: + resolution: {integrity: sha512-hD7wPe+vrPgx3U2X2b/wyTMtWobm660PygMGKrWWYTc9lvtY8DpNFDaU2CJQn1szLjGbn/aJ3g8WiXuKakrEkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 @@ -1414,8 +1417,8 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte-eslint-parser@1.3.1: - resolution: {integrity: sha512-0Iztj5vcOVOVkhy1pbo5uA9r+d3yaVoE5XPc9eABIWDOSJZ2mOsZ4D+t45rphWCOr0uMw3jtSG2fh2e7GvKnPg==} + svelte-eslint-parser@1.3.3: + resolution: {integrity: sha512-oTrDR8Z7Wnguut7QH3YKh7JR19xv1seB/bz4dxU5J/86eJtZOU4eh0/jZq4dy6tAlz/KROxnkRQspv5ZEt7t+Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 @@ -1648,6 +1651,11 @@ snapshots: eslint: 9.27.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.0(eslint@9.27.0(jiti@2.4.2))': + dependencies: + eslint: 9.27.0(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} '@eslint/compat@1.3.0(eslint@9.27.0(jiti@2.4.2))': @@ -1722,8 +1730,6 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.4': {} - '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.25': @@ -2184,10 +2190,10 @@ snapshots: dependencies: eslint: 9.27.0(jiti@2.4.2) - eslint-plugin-svelte@3.11.0(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4): + eslint-plugin-svelte@3.12.4(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) - '@jridgewell/sourcemap-codec': 1.5.4 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.27.0(jiti@2.4.2)) + '@jridgewell/sourcemap-codec': 1.5.5 eslint: 9.27.0(jiti@2.4.2) esutils: 2.0.3 globals: 16.3.0 @@ -2196,7 +2202,7 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) semver: 7.7.2 - svelte-eslint-parser: 1.3.1(svelte@5.33.4) + svelte-eslint-parser: 1.3.3(svelte@5.33.4) optionalDependencies: svelte: 5.33.4 transitivePeerDependencies: @@ -2671,7 +2677,7 @@ snapshots: transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.3.1(svelte@5.33.4): + svelte-eslint-parser@1.3.3(svelte@5.33.4): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 From c779ba6ddc6f6a084239dd14f17fa355c229314c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:54:53 +0000 Subject: [PATCH 09/93] Update dependency @sveltejs/kit to v2.42.2 (#2432) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 16883cd0693..28aac423bf3 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -23,10 +23,10 @@ importers: version: 5.2.7 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.8(@sveltejs/kit@2.39.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) + version: 3.0.8(@sveltejs/kit@2.42.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) '@sveltejs/kit': specifier: ^2.20.6 - version: 2.39.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 2.42.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 version: 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) @@ -493,8 +493,8 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.39.1': - resolution: {integrity: sha512-NdgBGHcf/3tXYzPRyQuvsmjI5d3Qp6uhgmlN3uurhyEMN0hMFhdUG83zmWBH8u/QXj6VBmPrKvUn0QXf+Q3/lQ==} + '@sveltejs/kit@2.42.2': + resolution: {integrity: sha512-FcNICFvlSYjPiAgk8BpqTEnXkaUj6I6wDwpQBxKMpsYhUc2Q5STgsVpXOG5LqwFpUAoLAXQ4wdWul7EcAG67JQ==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -1826,11 +1826,11 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.39.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.42.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.39.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.42.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) - '@sveltejs/kit@2.39.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/kit@2.42.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) From 69092dfd86b87738481ae4cdb118751453eb6998 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:02:56 +0200 Subject: [PATCH 10/93] Update dependency com.vanniktech:gradle-maven-publish-plugin to v0.34.0 (#2397) * Update dependency com.vanniktech:gradle-maven-publish-plugin to v0.34.0 * Fixed --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Christian Banse --- buildSrc/src/main/kotlin/cpg.publishing-conventions.gradle.kts | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/cpg.publishing-conventions.gradle.kts b/buildSrc/src/main/kotlin/cpg.publishing-conventions.gradle.kts index 41306e04174..ee97a1c1012 100644 --- a/buildSrc/src/main/kotlin/cpg.publishing-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/cpg.publishing-conventions.gradle.kts @@ -57,7 +57,7 @@ mavenPublishing { } } - publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + publishToMavenCentral() } // Sign the artifacts if the signingRequired property is set to true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6c331bd5323..b20977bccd2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ neo4j = "4.0.10" neo4j5 = "5.28.2" log4j = "2.24.0" spotless = "7.0.4" -publish = "0.33.0" +publish = "0.34.0" sootup = "2.0.0" slf4j = "2.0.16" clikt = "5.0.2" From 13b4c00b091bf443811e4c21b06996860efca9c2 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Thu, 25 Sep 2025 18:29:30 +0200 Subject: [PATCH 11/93] Moving `group` back to `allprojects` (#2435) --- build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7108204be17..f41e5da1fe6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,7 +40,9 @@ repositories { mavenCentral() } -group = "de.fraunhofer.aisec" +allprojects { + group = "de.fraunhofer.aisec" +} // Configure Dokka for the multi-module cpg project dokka { From bfd3eb3f2aa37a14cf1adbba87093cea036f3f5c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 07:26:39 +0200 Subject: [PATCH 12/93] Update dependency @eslint/compat to v1.4.0 (#2436) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 28aac423bf3..50bad9b2246 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -14,7 +14,7 @@ importers: devDependencies: '@eslint/compat': specifier: ^1.2.5 - version: 1.3.0(eslint@9.27.0(jiti@2.4.2)) + version: 1.4.0(eslint@9.27.0(jiti@2.4.2)) '@eslint/js': specifier: ^9.18.0 version: 9.27.0 @@ -272,11 +272,11 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/compat@1.3.0': - resolution: {integrity: sha512-ZBygRBqpDYiIHsN+d1WyHn3TYgzgpzLEcgJUxTATyiInQbKZz6wZb6+ljwdg8xeeOe4v03z6Uh6lELiw0/mVhQ==} + '@eslint/compat@1.4.0': + resolution: {integrity: sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^9.10.0 + eslint: ^8.40 || 9 peerDependenciesMeta: eslint: optional: true @@ -293,6 +293,10 @@ packages: resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.16.0': + resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1658,7 +1662,9 @@ snapshots: '@eslint-community/regexpp@4.12.1': {} - '@eslint/compat@1.3.0(eslint@9.27.0(jiti@2.4.2))': + '@eslint/compat@1.4.0(eslint@9.27.0(jiti@2.4.2))': + dependencies: + '@eslint/core': 0.16.0 optionalDependencies: eslint: 9.27.0(jiti@2.4.2) @@ -1676,6 +1682,10 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 + '@eslint/core@0.16.0': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 From 90a329bfa780b9d5c95d099b3b1da62789b971c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 05:29:16 +0000 Subject: [PATCH 13/93] Update dependency @sveltejs/kit to v2.43.5 (#2437) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 50bad9b2246..794c6d9e883 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -23,10 +23,10 @@ importers: version: 5.2.7 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.8(@sveltejs/kit@2.42.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) + version: 3.0.8(@sveltejs/kit@2.43.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) '@sveltejs/kit': specifier: ^2.20.6 - version: 2.42.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 2.43.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 version: 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) @@ -492,13 +492,18 @@ packages: peerDependencies: acorn: ^8.9.0 + '@sveltejs/acorn-typescript@1.0.6': + resolution: {integrity: sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==} + peerDependencies: + acorn: ^8.9.0 + '@sveltejs/adapter-static@3.0.8': resolution: {integrity: sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==} peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.42.2': - resolution: {integrity: sha512-FcNICFvlSYjPiAgk8BpqTEnXkaUj6I6wDwpQBxKMpsYhUc2Q5STgsVpXOG5LqwFpUAoLAXQ4wdWul7EcAG67JQ==} + '@sveltejs/kit@2.43.5': + resolution: {integrity: sha512-44Mm5csR4mesKx2Eyhtk8UVrLJ4c04BT2wMTfYGKJMOkUqpHP5KLL2DPV0hXUA4t4+T3ZYe0aBygd42lVYv2cA==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -1832,18 +1837,18 @@ snapshots: dependencies: acorn: 8.14.1 - '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.42.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.43.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.42.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.43.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) - '@sveltejs/kit@2.42.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/kit@2.43.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) '@types/cookie': 0.6.0 acorn: 8.15.0 From 090520d6f0f0c9afbfbf06c52f9bff6418c4c70f Mon Sep 17 00:00:00 2001 From: Robert Haimerl Date: Thu, 2 Oct 2025 10:32:46 +0200 Subject: [PATCH 14/93] Abstract Value Analysis (#1759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * create initial Size evaluator for modifiable lists * move all collection dependant logic into collection class * add support for static arrays * implement narrowing and widening operations * add narrowing and widening to loop analysis * handle branches in analysis * improve loops by preprocessing, better narrowing and valid starting ranges * improve subtraction by handling 0 as lowest possible value * mark branching nodes as nodes with an effect * correctly identify nodes of deeper branch layers in a loop * add documentation * temporarily repurpose cpg-neo4j for debugging * rework lattice intervals to support negative values * rename target package to circumvent gitignore * implement comparable for the LatticeInterval * implement the IntervalState * update the evaluator name in tests * implement equals check for intervals * add method stub for condition evaluation * Spotless * Squashed commit of the following: commit 5c20b05cfab4c0ea1e32a5e01378a512275f943c Author: Robert Haimerl Date: Wed Oct 16 13:25:06 2024 +0200 fix breaking merge changes commit cc8eb48c1d4134cbbdc637fda653e2e4b2e4a4b7 Merge: 3e3c094ef2 344ea58840 Author: Robert Haimerl Date: Wed Oct 16 13:08:46 2024 +0200 Merge branch 'rh/abstract-value-analysis' into rh/abstract-value-analysis-worklist # Conflicts: # cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractEvaluator.kt # cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt commit 3e3c094ef2fed2e041a994bbbe92eb0871a224d0 Author: Robert Haimerl Date: Wed Oct 16 13:04:45 2024 +0200 remove redundant pushes to the worklist itself commit 6d731f27d2dcad094a623be758b6f97c9cf59ba4 Author: Robert Haimerl Date: Wed Oct 16 13:04:21 2024 +0200 override methods to use custom functionality in IntervalStates commit 1eb5a5194396cd8c9ff2a1f73df0d8e0078a17c5 Author: Robert Haimerl Date: Wed Oct 16 13:03:57 2024 +0200 enhance analysis for simple value operations commit 108f374a6ccddf2f3723b96a9c43ca74d9c55bb4 Author: Robert Haimerl Date: Wed Oct 16 09:56:22 2024 +0200 join intervals for multiple EOG (branch joins) commit d4383b44f494afbceb9e1fbf1cb0167031e20438 Author: Robert Haimerl Date: Wed Oct 16 09:33:41 2024 +0200 revert the change to the worklist pop to make it FIFO again commit 46798c5fce48f78876adfd947043d4999ef94c8a Author: Robert Haimerl Date: Wed Oct 16 09:33:23 2024 +0200 return a new altered state instead of directly modifying the current state commit c6346465864d97e2eb783e7b2e7044f2f621513e Author: Robert Haimerl Date: Mon Oct 14 13:12:46 2024 +0200 remove getInitialRange from the evaluator commit f984a2cce52a084942be6c3555ad6ac14cb50505 Author: Robert Haimerl Date: Mon Oct 14 13:11:58 2024 +0200 remove the "getInitialRange" method for values and instead mark declarations as operations with effect commit beb4d3897c841bc4a3d66314f5bdae11aafd1ae4 Author: Robert Haimerl Date: Mon Oct 14 12:26:22 2024 +0200 simplify evaluator to only use one worklist without special handling for loops and branches commit 6ec77dc6ebb7f3846bf319ee6d0ad45c8198c941 Author: Robert Haimerl Date: Mon Oct 14 12:25:04 2024 +0200 add "until" to iteration, add state information to Worklist, fix pop order commit 253debb2f55ff4ba5f6c4525e17d8957b0151d5c Author: Robert Haimerl Date: Mon Oct 14 12:23:17 2024 +0200 remove all modes from the IntervalState commit 31ca4ac6e3dc284c49c7e3afee56174b225f0d2f Author: Robert Haimerl Date: Mon Oct 14 12:17:45 2024 +0200 remove boolean information about whether the operation had an impact commit ccd839e6b64acb4c4861b3693d95106f6bb00e96 Author: Robert Haimerl Date: Mon Oct 14 09:17:11 2024 +0200 add three different state modes commit e0e1a28274f7d1ade5b4caefd2279acb7b6bffb5 Author: Robert Haimerl Date: Mon Oct 7 12:08:52 2024 +0200 rewrite the handleBranch branch commit 672f27f898242a0f3c144ee9fe78f551482f24cb Author: Robert Haimerl Date: Mon Oct 7 11:45:10 2024 +0200 rewrite the handleLoop branch commit 33f35313bfb573ed2b614a9308f52f41af4a5197 Author: Robert Haimerl Date: Mon Oct 7 10:54:15 2024 +0200 rewrite the applyEffect branch commit ca88bb7c83c4628d53f825ac0e45c3e67d07c4e8 Author: Robert Haimerl Date: Mon Oct 7 10:20:39 2024 +0200 rewrite evaluate function to use "iterateEOG" * fix LatticeInterval.push to join with the previous value as it is already propagated from predecessors * Fix Declaration to only effect matching integers * correctly propagate widening only within loops * added second evaluate call with more defined arguments * support functions with side effects by creating a function-local evaluator * moved side effect analysis to list instead of integer... * add new 'assignMinus' expression and add the name as code when creating a reference * cleanup * add tests for the new Evaluator * first implementation for other integer operations * add prefix versions of the inc and dec operator * Add test case for all implemented Integer operations * add auto-generated hashcode function * cleanup and documentation * remove old code * import cleanup * automatically switch interval bounds if the lower bound is greater than the upper bound * remove min and max comparisons after multiplication as the constructor now handles the order * convert Bounded from data class to real class to remove constructor variables * fix division to estimate x / āˆž = 0 * simplify division cases * Fix invalid division cases * make modulo calculation more concise * beautify toString representation * add first LatticeInterval tests * fix meet of intervals when they do not overlap * add equals operator for the Interval wrapper * add tests for meet, widen, narrow and the wrapper * fix meet test method * change default of plusAssign and minusAssign to lose all information * add tests for the Integer Value * check name of array declaration * move code block for function side effects further up * add tests for array and mutable list * remove unfinished code block in MutableList * made spotless * changed the name of the Value classes to prevent confusion with builtin classes * Some renaming and extended test * formatting * Do not stop on target node and change int to long * Template for eventually using new iteration * Fix comment * Minor cosmetic changes * Move code to correct location * Several updates, missing widening and narrowing for integer tests * Some fixes * Fix integer tests * Some idea on integrating widening and narrowing * Try to integrate widening * Integrate widening and fix comparisons * Try to include conditions * Fix * Also handle negated condition * Redesign of list size evaluation * Small cleanup * Rework array value * Small cleanup * Fix list removeall * Size evaluation for sets * test set size evaluator * Improvements and more tests for sets * Integration into value evaluator system and query API * Revert unnecessary changes in EOGWorklist.kt * Split division by 0 and other test * Try to fix problematic integer lookup - no success * somewhat fix problem with equal but non-identical Integer objects used as keys * Fix min and max operations * Add more operators, implement shl * right shifting * bitwise and and or * trigger build * define some of the exceptional cases to avoid exceptions * Several fixes in tests and in minus * throw less exceptions and catch them * Fix asserts * Less unsupported operators * Some cleanup * Fix test * Remove commented out code --------- Co-authored-by: Robert Haimerl Co-authored-by: KuechA <31155350+KuechA@users.noreply.github.com> Co-authored-by: Alexander Kuechler --- .../abstracteval/AbstractIntervalEvaluator.kt | 567 +++++++++++++++ .../analysis/abstracteval/LatticeInterval.kt | 660 ++++++++++++++++++ .../analysis/abstracteval/value/ArrayValue.kt | 133 ++++ .../abstracteval/value/IntegerValue.kt | 519 ++++++++++++++ .../value/MutableCollectionSize.kt | 162 +++++ .../abstracteval/value/MutableListSize.kt | 163 +++++ .../abstracteval/value/MutableSetSize.kt | 173 +++++ .../cpg/analysis/abstracteval/value/Value.kt | 61 ++ .../aisec/cpg/passes/UnreachableEOGPass.kt | 2 +- .../de/fraunhofer/aisec/cpg/query/Query.kt | 86 ++- .../abstracteval/AbstractEvaluatorTest.kt | 243 +++++++ .../abstracteval/LatticeIntervalTest.kt | 390 +++++++++++ .../abstracteval/value/ArrayValueTest.kt | 119 ++++ .../abstracteval/value/IntegerValueTest.kt | 534 ++++++++++++++ .../value/MutableListValueTest.kt | 276 ++++++++ .../abstracteval/value/MutableSetValueTest.kt | 332 +++++++++ .../fraunhofer/aisec/cpg/query/QueryTest.kt | 2 +- .../cpg/testcases/AbstractEvaluationTests.kt | 251 +++++++ .../fraunhofer/aisec/cpg/testcases/Query.kt | 48 +- .../aisec/cpg/graph/builder/Fluent.kt | 223 ++++-- .../aisec/cpg/helpers/EOGWorklist.kt | 10 +- .../functional/BasicLatticesRedesign.kt | 153 +++- 22 files changed, 4996 insertions(+), 111 deletions(-) create mode 100644 cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractIntervalEvaluator.kt create mode 100644 cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeInterval.kt create mode 100644 cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValue.kt create mode 100644 cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValue.kt create mode 100644 cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableCollectionSize.kt create mode 100644 cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListSize.kt create mode 100644 cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableSetSize.kt create mode 100644 cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/Value.kt create mode 100644 cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractEvaluatorTest.kt create mode 100644 cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeIntervalTest.kt create mode 100644 cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValueTest.kt create mode 100644 cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValueTest.kt create mode 100644 cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListValueTest.kt create mode 100644 cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableSetValueTest.kt create mode 100644 cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/AbstractEvaluationTests.kt diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractIntervalEvaluator.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractIntervalEvaluator.kt new file mode 100644 index 00000000000..155e7df854e --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractIntervalEvaluator.kt @@ -0,0 +1,567 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.value.* +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration +import de.fraunhofer.aisec.cpg.graph.edges.flows.EvaluationOrder +import de.fraunhofer.aisec.cpg.graph.firstParentOrNull +import de.fraunhofer.aisec.cpg.helpers.functional.* +import de.fraunhofer.aisec.cpg.passes.objectIdentifier +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.putAll +import kotlin.collections.set +import kotlin.reflect.KClass +import kotlin.reflect.full.createInstance +import kotlin.to +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +val log: Logger = LoggerFactory.getLogger(AbstractIntervalEvaluator::class.java) + +class TupleState( + innerLattice1: DeclarationState, + innerLattice2: Lattice, +) : + TupleLattice, NewIntervalStateElement>( + innerLattice1 as Lattice>, + innerLattice2, + ) { + + override fun lub( + one: TupleStateElement, + two: TupleStateElement, + allowModify: Boolean, + widen: Boolean, + ): TupleStateElement { + return if (allowModify) { + innerLattice1.lub(one = one.first, two = two.first, allowModify = true, widen = widen) + innerLattice2.lub(one = one.second, two = two.second, allowModify = true, widen = false) + one + } else { + Element( + innerLattice1.lub( + one = one.first, + two = two.first, + allowModify = false, + widen = widen, + ), + innerLattice2.lub( + one = one.second, + two = two.second, + allowModify = false, + widen = false, + ), + ) + } + } +} + +typealias TupleStateElement = + TupleLattice.Element, NewIntervalStateElement> + +class DeclarationState(innerLattice: Lattice) : + MapLattice(innerLattice) { + override val bottom: DeclarationStateElement + get() = DeclarationStateElement() + + override fun lub( + one: Element, + two: Element, + allowModify: Boolean, + widen: Boolean, + ): Element { + val result = super.lub(one, two, allowModify, widen) + if (result is DeclarationStateElement) { + // If the result is a DeclarationStateElement, we can return it directly + return result + } else { + return DeclarationStateElement(result) + } + } + + override fun glb( + one: Element, + two: Element, + ): Element { + val result = super.glb(one, two) + if (result is DeclarationStateElement) { + // If the result is a DeclarationStateElement, we can return it directly + return result + } else { + return DeclarationStateElement(result) + } + } + + class DeclarationStateElement(expectedMaxSize: Int) : + Element(expectedMaxSize) { + constructor() : this(32) + + constructor(m: Map) : this(m.size) { + putAll(m) + } + + constructor( + entries: Collection> + ) : this(entries.size) { + putAll(entries) + } + + constructor(vararg entries: Pair) : this(entries.size) { + putAll(entries) + } + + override fun equals(other: Any?): Boolean { + return other is DeclarationStateElement && this.compare(other) == Order.EQUAL + } + + override fun hashCode(): Int { + return super.hashCode() + } + + override fun duplicate(): DeclarationStateElement { + return DeclarationStateElement( + this.map { (k, v) -> Pair(k, v.duplicate()) } + ) + } + + fun findKey(nodeId: NodeId): NodeId { + return if (nodeId is Integer) { + this.entries.singleOrNull { it.key == nodeId }?.key ?: nodeId + } else nodeId + } + + override fun containsKey(key: NodeId?): Boolean { + return if (key is Integer) { + this.entries.singleOrNull { it.key == key } != null + } else { + super.containsKey(key) + } + } + + override fun put(key: NodeId?, value: IntervalLattice.Element?): IntervalLattice.Element? { + val actualKey = key?.let { findKey(it) } + return super.put(actualKey, value) + } + + /** + * Retrieves the interval element for the given [nodeId] from the declaration state element. + * + * @param nodeId The identifier of the node. + * @return The [IntervalLattice.Element] for the node, or null if not found. + */ + override operator fun get(nodeId: NodeId): IntervalLattice.Element? { + return if (nodeId is Integer) { + this.entries.singleOrNull { it.key == nodeId }?.value + } else { + super.get(nodeId) + } + } + } +} + +typealias NewIntervalState = MapLattice + +typealias NewIntervalStateElement = MapLattice.Element + +class IntervalLattice() : + Lattice, + HasWidening, + HasNarrowing { + override var elements: Set = setOf() + override val bottom: Element = Element(LatticeInterval.BOTTOM) + + override fun lub(one: Element, two: Element, allowModify: Boolean, widen: Boolean): Element { + val oneElem = one.element + val twoElem = two.element + if (allowModify) { + when { + widen -> { + one.element = twoElem.widen(oneElem) + } + oneElem == LatticeInterval.TOP || twoElem == LatticeInterval.BOTTOM -> { + // Nothing to do as one is already the top or bigger than two. + } + twoElem == LatticeInterval.TOP -> { + // Set one to TOP too. + one.element = LatticeInterval.TOP + } + oneElem == LatticeInterval.BOTTOM -> { + // Set one to two as it is the bottom. + one.element = twoElem + } + oneElem is LatticeInterval.Bounded && twoElem is LatticeInterval.Bounded -> { + // If both are bounded, we can calculate the new bounds. + val newLower = minOf(oneElem.lower, twoElem.lower) + val newUpper = maxOf(oneElem.upper, twoElem.upper) + one.element = LatticeInterval.Bounded(newLower, newUpper) + } + else -> { + log.warn( + "Cannot handle this case in IntervalLattice.lub: $oneElem and $twoElem" + ) + Element(LatticeInterval.TOP) + } + } + return one + } else { + return when { + widen -> { + Element(twoElem.widen(oneElem)) + } + oneElem == LatticeInterval.TOP || twoElem == LatticeInterval.TOP -> { + Element(LatticeInterval.TOP) + } + twoElem == LatticeInterval.BOTTOM -> { + one.duplicate() + } + oneElem == LatticeInterval.BOTTOM -> { + two.duplicate() + } + oneElem is LatticeInterval.Bounded && twoElem is LatticeInterval.Bounded -> { + val newLower = minOf(oneElem.lower, twoElem.lower) + val newUpper = maxOf(oneElem.upper, twoElem.upper) + Element(LatticeInterval.Bounded(newLower, newUpper)) + } + else -> { + log.warn( + "Cannot handle this case in IntervalLattice.lub: $oneElem and $twoElem" + ) + Element(LatticeInterval.TOP) + } + } + } + } + + override fun glb(one: Element, two: Element): Element { + val oneElem = one.element + val twoElem = two.element + return when { + oneElem == LatticeInterval.TOP && twoElem == LatticeInterval.TOP -> { + Element(LatticeInterval.TOP) + } + + oneElem == LatticeInterval.BOTTOM || twoElem == LatticeInterval.BOTTOM -> { + Element(LatticeInterval.BOTTOM) + } + oneElem == LatticeInterval.TOP -> { + two.duplicate() + } + twoElem == LatticeInterval.TOP -> { + one.duplicate() + } + + oneElem is LatticeInterval.Bounded && twoElem is LatticeInterval.Bounded -> { + val newLower = maxOf(oneElem.lower, twoElem.lower) + val newUpper = minOf(oneElem.upper, twoElem.upper) + Element(LatticeInterval.Bounded(newLower, newUpper)) + } + + else -> { + log.warn("Cannot handle this case in IntervalLattice.glb: $oneElem and $twoElem") + Element(LatticeInterval.TOP) + } + } + } + + override fun compare(one: Element, two: Element): Order { + return one.compare(two) + } + + override fun duplicate(one: Element): Element { + return one.duplicate() + } + + override fun widen(one: Element, two: Element): Element { + return Element(one.element.widen(two.element)) + } + + override fun narrow(one: Element, two: Element): Element { + return Element(one.element.narrow(two.element)) + } + + class Element(var element: LatticeInterval) : Lattice.Element { + override fun toString(): String { + return "IntervalLattice.Element(elements=$element)" + } + + override fun compare(other: Lattice.Element): Order { + if (other !is Element) { + throw IllegalArgumentException("Cannot compare IntervalLattice.Element with $other") + } + val thisBounded = this.element as? LatticeInterval.Bounded + val otherBounded = other.element as? LatticeInterval.Bounded + return when { + this.element is LatticeInterval.TOP && other.element is LatticeInterval.TOP -> + Order.EQUAL + this.element is LatticeInterval.BOTTOM && other.element is LatticeInterval.BOTTOM -> + Order.EQUAL + this.element is LatticeInterval.TOP -> Order.GREATER + other.element is LatticeInterval.TOP -> Order.LESSER + this.element is LatticeInterval.BOTTOM -> Order.LESSER + other.element is LatticeInterval.BOTTOM -> Order.GREATER + thisBounded != null && + thisBounded.lower == otherBounded?.lower && + thisBounded.upper == otherBounded.upper -> Order.EQUAL + thisBounded != null && + otherBounded != null && + thisBounded.lower >= otherBounded.lower && + thisBounded.upper <= otherBounded.upper -> Order.LESSER + thisBounded != null && + otherBounded != null && + thisBounded.lower <= otherBounded.lower && + thisBounded.upper >= otherBounded.upper -> Order.GREATER + else -> Order.UNEQUAL + } + } + + override fun duplicate(): Element { + return Element(this.element) // TODO: Implement a deep copy! + } + } +} + +/** + * Performs abstract interval analysis for a single [Value] in the code property graph (CPG). + * + * This evaluator computes the interval (lower and upper bounds) of a value at a specific program + * point by traversing the EOG (Execution Order Graph) from the value's declaration to the target + * node. It uses a tuple lattice to track both declaration-specific and general interval state. + * + * Typical usage: + * ```kotlin + * val evaluator = AbstractIntervalEvaluator() + * val interval = evaluator.evaluate(node, MyValueType::class) + * ``` + */ +class AbstractIntervalEvaluator { + /** The type of value being analyzed. Set during evaluation. */ + private lateinit var analysisType: KClass> + + /** + * Evaluates the interval of a value at the given [node], using the specified [targetType]. + * + * @param node The node whose value interval is to be evaluated (e.g., a reference). + * @param targetType The [Value] type to analyze. + * @return The computed [LatticeInterval] for the value at this node, or + * [LatticeInterval.BOTTOM] if not found. + */ + fun evaluate(node: Node, targetType: KClass>): LatticeInterval { + val startNode = + node.firstParentOrNull() ?: return LatticeInterval.BOTTOM + return evaluate(startNode, node, targetType, LatticeInterval.BOTTOM) + } + + /** + * Evaluates the interval of a value at [targetNode], starting from [start], with the given + * [type] and initial [interval]. + * + * @param start The node where analysis begins (typically the variable's declaration). + * @param targetNode The node at which to compute the value's interval. + * @param type The [Value] type to analyze. + * @param interval The initial interval value (default: [LatticeInterval.BOTTOM]). + * @return The computed [LatticeInterval] for the value at [targetNode]. + */ + fun evaluate( + start: Node, + targetNode: Node, + type: KClass>, + interval: LatticeInterval = LatticeInterval.BOTTOM, // TODO: Maybe should be top? + ): LatticeInterval { + analysisType = type + val declarationState = DeclarationState(IntervalLattice()) + val intervalState = NewIntervalState(IntervalLattice()) + val startState = TupleState(declarationState, intervalState) + + // evaluate effect of each operation on the list until we reach "node" + val startStateElement = startState.bottom + val startInterval = startStateElement.second + intervalState.push(startInterval, start, interval) + declarationState.push(startStateElement.first, start, interval) + + val finalState = + startState.iterateEOG( + start.nextEOGEdges, + startStateElement, + ::handleNode, + strategy = Lattice.Strategy.WIDENING_NARROWING, + ) + return finalState.second.get(targetNode)?.element ?: LatticeInterval.BOTTOM + } + + /** + * Handles the effect of a node during EOG traversal, updating the analysis state. This function + * changes the state depending on the current node. This is the handler used in `iterateEOG` to + * correctly handle complex statements. + * + * @param lattice The tuple lattice representing current analysis state. + * @param currentEdge The current EOG edge being processed. + * @param currentState The current tuple state element. + * @return The updated tuple state element after applying the node's effect. + */ + private fun handleNode( + lattice: Lattice>, + currentEdge: EvaluationOrder, + currentState: TupleStateElement, + ): TupleStateElement { + val currentNode = currentEdge.end + val newState = currentState + + analysisType + .createInstance() + .applyEffect( + lattice = lattice as TupleState, + state = newState, + node = currentNode, + edge = currentEdge, + ) + + return newState + } +} + +/** + * Retrieves the interval value for the given [node] from this tuple state element. + * + * @param node The [Node] whose interval is to be fetched. + * @return The [LatticeInterval] associated with the node, or [LatticeInterval.TOP] if not found. + */ +fun TupleStateElement.intervalOf(node: Node): LatticeInterval { + val id = + node.objectIdentifier()?.let { tmpId -> + this.first.keys.singleOrNull { it == tmpId } ?: (tmpId as? NodeId) + } ?: node as? NodeId ?: TODO() + return this.first[id]?.element ?: LatticeInterval.TOP +} + +/** + * Updates the declaration state for [node] with the specified [interval] in the current tuple state + * element. It overwrites the existing interval for the node if it exists, or adds a new entry if it + * does not. It does not compute `lub` with the existing entry! + * + * @param current The current tuple state element. + * @param node The [Node] whose declaration state is to be updated. + * @param interval The new [LatticeInterval] to set. + * @return The updated tuple state element. + */ +fun TupleState.changeDeclarationState( + current: TupleStateElement, + node: Node, + interval: LatticeInterval, +): TupleStateElement { + val id = + (node.objectIdentifier() as? NodeId)?.let { tmpId -> + current.first.keys.singleOrNull { it == tmpId } ?: tmpId + } ?: node as NodeId ?: TODO() + current.first[id] = IntervalLattice.Element(interval) + return current +} + +/** + * Pushes a new interval for [node] into the declaration state, merging with the existing state. + * + * @param current The current tuple state element. + * @param node The [Node] to update. + * @param interval The [LatticeInterval] to push. + * @return The updated tuple state element. + */ +fun TupleState.pushToDeclarationState( + current: TupleStateElement, + node: Node, + interval: LatticeInterval, +): TupleStateElement { + val id = + (node.objectIdentifier() as? NodeId)?.let { tmpId -> + current.first.keys.singleOrNull { it == tmpId } ?: tmpId + } ?: node as NodeId ?: TODO() + this.innerLattice1.lub( + current.first, + DeclarationState.DeclarationStateElement(id to IntervalLattice.Element(interval)), + allowModify = true, + ) + return current +} + +/** + * Pushes a new interval for [node] into the general state, merging with the existing state. + * + * @param current The current tuple state element. + * @param node The [Node] to update. + * @param interval The [LatticeInterval] to push. + * @return The updated tuple state element. + */ +fun TupleState.pushToGeneralState( + current: TupleStateElement, + node: Node, + interval: LatticeInterval, +): TupleStateElement { + this.innerLattice2.lub( + current.second, + NewIntervalStateElement(node to IntervalLattice.Element(interval)), + allowModify = true, + ) + return current +} + +/** + * Pushes a new interval for [start] into the declaration state lattice. + * + * @param current The current declaration state element. + * @param start The node identifier to update. + * @param interval The [LatticeInterval] to push. + */ +private fun DeclarationState.push( + current: DeclarationState.DeclarationStateElement, + start: NodeId, + interval: LatticeInterval, +) { + this.lub( + current, + DeclarationState.DeclarationStateElement(start to IntervalLattice.Element(interval)), + allowModify = true, + ) +} + +/** + * Pushes a new interval for [start] into the general interval state lattice. + * + * @param current The current interval state element. + * @param start The [Node] to update. + * @param interval The [LatticeInterval] to push. + */ +private fun NewIntervalState.push( + current: NewIntervalStateElement, + start: Node, + interval: LatticeInterval, +) { + this.lub( + current, + NewIntervalStateElement(start to IntervalLattice.Element(interval)), + allowModify = true, + ) +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeInterval.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeInterval.kt new file mode 100644 index 00000000000..8a1e6c1dfe4 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeInterval.kt @@ -0,0 +1,660 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval + +import kotlin.math.pow +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * The [LatticeInterval] class implements the functionality of intervals that is needed for the + * [AbstractIntervalEvaluator]. It is either a [BOTTOM] object signaling no knowledge or a [Bounded] + * object with a lower and upper [Bound]. Each [Bound] can then be [Bound.NEGATIVE_INFINITE], + * [Bound.INFINITE] or a [Bound.Value]. This class implements many convenience methods to handle the + * [LatticeInterval]. + */ +sealed class LatticeInterval : Comparable { + companion object { + val log: Logger = LoggerFactory.getLogger(LatticeInterval::class.java) + } + + /** Explicit representation of the bottom element of the lattice. */ + object BOTTOM : LatticeInterval() + + object TOP : + LatticeInterval.Bounded( + LatticeInterval.Bound.NEGATIVE_INFINITE, + LatticeInterval.Bound.INFINITE, + ) + + fun duplicate(): LatticeInterval { + return when (this) { + is BOTTOM -> BOTTOM + is TOP -> TOP + is Bounded -> Bounded(lower, upper) + } + } + + /** + * Explicit representation of an interval with a minimal value [lower] and a maximal value + * [upper]. + */ + open class Bounded(arg1: Bound, arg2: Bound) : LatticeInterval() { + val lower: Bound + val upper: Bound + + constructor( + arg1: Number, + arg2: Number, + ) : this(Bound.Value(arg1.toLong()), Bound.Value(arg2.toLong())) + + constructor(arg1: Long, arg2: Bound) : this(Bound.Value(arg1), arg2) + + constructor(arg1: Bound, arg2: Long) : this(arg1, Bound.Value(arg2)) + + // Automatically switch the arguments if the upper bound is lower than the lower bound + init { + if (arg1 > arg2) { + lower = arg2 + upper = arg1 + } else { + lower = arg1 + upper = arg2 + } + } + + override fun toString(): String { + return "[$lower, $upper]" + } + } + + /** + * Representation of a value of the interval. It can be a concrete integer ([Long]) value, + * negative or positive infinity. + */ + sealed class Bound : Comparable { + /** The [Bound] that represents a concrete integer value in the interval. */ + data class Value(val value: Long) : Bound() { + override fun toString(): String { + return value.toString() + } + } + + /** + * The [Bound] that represents negative infinity. It is used to represent the lower bound of + * an interval that can go infinitely low. + */ + data object NEGATIVE_INFINITE : Bound() + + /** + * The [Bound] that represents positive infinity. It is used to represent the lower bound of + * an interval that can go infinitely high. + */ + data object INFINITE : Bound() + + override fun compareTo(other: Bound): Int { + return when { + this is NEGATIVE_INFINITE && other !is NEGATIVE_INFINITE -> -1 + this is INFINITE && other !is INFINITE -> 1 + other is NEGATIVE_INFINITE && this !is NEGATIVE_INFINITE -> 1 + other is INFINITE && this !is INFINITE -> -1 + this is Value && other is Value -> this.value.compareTo(other.value) + else -> 0 + } + } + + override fun toString(): String { + return when (this) { + is Value -> value.toString() + is INFINITE -> "INFINITE" + is NEGATIVE_INFINITE -> "NEGATIVE_INFINITE" + } + } + + // Addition operator + operator fun plus(other: Bound): Bound { + return when { + this is INFINITE || other is INFINITE -> INFINITE + this is NEGATIVE_INFINITE || other is NEGATIVE_INFINITE -> NEGATIVE_INFINITE + this is Value && other is Value -> { + Value(this.value + other.value) + } + else -> { + throw IllegalArgumentException("Unsupported bound type $this and $other") + } + } + } + + // Subtraction operator + operator fun minus(other: Bound): Bound { + return when { + this is INFINITE || other is NEGATIVE_INFINITE -> INFINITE + this is NEGATIVE_INFINITE || other is INFINITE -> NEGATIVE_INFINITE + this is Value && other is Value -> { + Value(this.value - other.value) + } + else -> throw IllegalArgumentException("Unsupported bound type $this and $other") + } + } + } + + override fun compareTo(other: LatticeInterval): Int { + // Comparing two Intervals. They are treated as equal if they overlap + // BOTTOM intervals are considered "smaller" than known intervals + return when { + this is BOTTOM && other !is BOTTOM -> -1 + other is BOTTOM && this !is BOTTOM -> 1 + this is Bounded && other is Bounded -> { + when { + this.lower > other.upper -> 1 + this.upper < other.lower -> -1 + else -> 0 + } + } + else -> 0 + } + } + + override fun equals(other: Any?): Boolean { + // Equals check is only true if both Intervals are true or have the same boundaries + // Not the same as a zero result in compareTo! + return when (other) { + !is LatticeInterval -> return false + is BOTTOM -> this is BOTTOM + is Bounded -> { + when (this) { + is Bounded -> { + this.lower == other.lower && this.upper == other.upper + } + else -> false + } + } + else -> false + } + } + + /** + * Implements the left shift operation for intervals. It shifts the lower and upper bounds + * according to the left shift operation defined by the [other] interval. The [maxBits] + * parameter defines the bitwidth of the resulting datatype. The result is [BOTTOM] if either of + * the values is [BOTTOM]. The result is [TOP] if either of the values is [TOP] or if the shift + * operation is undefined (e.g., shifting by a negative value or exceeding the maximum bits). + * Otherwise, we compute the new range by multiplying this interval with `[2^min(other), + * 2^max(other)]`. + */ + fun shl(other: LatticeInterval, maxBits: Int): LatticeInterval { + if (this is BOTTOM || other is BOTTOM) return BOTTOM + if (this is TOP || other is TOP) return TOP + + if (this is Bounded && other is Bounded) { + val thisLower = this.lower + val otherLower = other.lower + val thisUpper = this.upper + val otherUpper = other.upper + if ( + thisLower is Bound.Value && + thisUpper is Bound.Value && + otherLower is Bound.Value && + otherUpper is Bound.Value + ) { + return when { + otherLower.value < 0 -> { + TOP // Undefined behavior for negative shifts + } + otherUpper.value > maxBits -> { + TOP // Undefined behavior if we exceed the maximum bits + } + thisLower.value < 0 -> { + TOP // Undefined behavior for negative shifts + } + else -> { + val result = + this * + Bounded( + 2.toDouble().pow(otherLower.value.toDouble()).toLong(), + 2.toDouble().pow(otherUpper.value.toDouble()).toLong(), + ) + if ( + result is Bounded && + result.upper is Bound.Value && + result.upper.value >= 2.0.pow(maxBits - 1.0).toLong() + ) { + TOP // If the upper bound exceeds the maximum bits, we return TOP + } else result + } + } + } else return TOP + } else { + return TOP // Cannot determine bounds + } + } + + /** + * Implements the right shift operation for intervals. It shifts the lower and upper bounds + * according to the right shift operation defined by the [other] interval. The [maxBits] + * parameter defines the bitwidth of the resulting datatype. The result is [BOTTOM] if either of + * the values is [BOTTOM]. The result is [TOP] if either of the values is [TOP] or if the shift + * operation is undefined (e.g., shifting by a negative value or exceeding the maximum bits). + * Otherwise, we compute the new range by dividing this interval with `[2^min(other), + * 2^max(other)]`. + */ + fun shr(other: LatticeInterval, maxBits: Int): LatticeInterval { + if (this is BOTTOM || other is BOTTOM) return BOTTOM + if (this is TOP || other is TOP) return TOP + + if (this is Bounded && other is Bounded) { + val thisLower = this.lower + val otherLower = other.lower + val thisUpper = this.upper + val otherUpper = other.upper + if ( + thisLower is Bound.Value && + thisUpper is Bound.Value && + otherLower is Bound.Value && + otherUpper is Bound.Value + ) { + return when { + otherLower.value < 0 -> { + TOP // Undefined behavior for negative shifts + } + + otherUpper.value > maxBits -> { + TOP // Undefined behavior if we exceed the maximum bits + } + + thisLower.value < 0 -> { + TOP // Undefined behavior for negative shifts + } + + else -> { + return this / + Bounded( + 2.toDouble().pow(otherLower.value.toDouble()).toLong(), + 2.toDouble().pow(otherUpper.value.toDouble()).toLong(), + ) + } + } + } else return TOP + } else { + return TOP // Cannot determine bounds + } + } + + fun bitwiseAnd(other: LatticeInterval): LatticeInterval { + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + val minUpper = min(this.upper, other.upper) + if ( + (this.lower as? Bound.Value)?.value?.let { it >= 0 } == true && + (other.lower as? Bound.Value)?.value?.let { it >= 0 } == true && + minUpper is Bound.Value && + minUpper.value > 0 + ) { + // We only compute this for non-negative values + Bounded(Bound.Value(0), minUpper) + } else TOP + } + else -> { + log.warn("Unsupported interval type $this and $other") + TOP + } + } + } + + fun bitwiseOr(other: LatticeInterval): LatticeInterval { + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + val maxLower = max(this.lower, other.lower) + val maxUpper = max(this.upper, other.upper) + if (maxUpper is Bound.Value) { + val upper = + 2.0.pow((maxUpper.value.takeHighestOneBit() + 1).toDouble()).toLong() - 1 + if (maxLower is Bound.Value && maxLower.value > 0) { + Bounded(maxLower, upper) + } else TOP + } else TOP + } + else -> { + log.warn("Unsupported interval type $this and $other") + TOP + } + } + } + + operator fun plus(other: LatticeInterval): LatticeInterval { + // Addition operator + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + try { + val newLower = addBounds(this.lower, other.lower, BoundType.LOWER) + val newUpper = addBounds(this.upper, other.upper, BoundType.UPPER) + Bounded(newLower, newUpper) + } catch (e: IllegalArgumentException) { + // Catch the exception if the operation is not defined and return TOP + log.warn("Plus not defined for $this and $other: ${e.message}") + TOP + } + } + else -> { + log.warn("Unsupported interval type $this and $other") + TOP + } + } + } + + operator fun minus(other: LatticeInterval): LatticeInterval { + // Subtraction operator + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + try { + val newLower = subtractBounds(this.lower, other.upper) + val newUpper = subtractBounds(this.upper, other.lower) + Bounded(newLower, newUpper) + } catch (e: IllegalArgumentException) { + // Catch the exception if the operation is not defined and return TOP + log.warn("Minus not defined for $this and $other: ${e.message}") + TOP + } + } + else -> { + log.warn("Unsupported interval type $this and $other") + TOP + } + } + } + + operator fun times(other: LatticeInterval): LatticeInterval { + // Multiplication operator + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + try { + val newLower = multiplyBounds(this.lower, other.lower) + val newUpper = multiplyBounds(this.upper, other.upper) + Bounded(newLower, newUpper) + } catch (e: IllegalArgumentException) { + // Catch the exception if the operation is not defined and return TOP + log.warn("Times not defined for $this and $other: ${e.message}") + TOP + } + } + else -> { + log.warn("Unsupported interval type $this and $other") + TOP + } + } + } + + operator fun div(other: LatticeInterval): LatticeInterval { + // Division operator + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + try { + + val newLower = divideBounds(this.lower, other.lower) + val newUpper = divideBounds(this.upper, other.upper) + Bounded(newLower, newUpper) + } catch (e: IllegalArgumentException) { + // Catch the exception if the operation is not defined and return TOP + log.warn("Division not defined for $this and $other: ${e.message}") + TOP + } + } + else -> { + log.warn("Unsupported interval type $this and $other") + TOP + } + } + } + + operator fun rem(other: LatticeInterval): LatticeInterval { + // Modulo operator + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + try { + val lowerBracket = modulateBounds(this.lower, other.lower) + val upperBracket = modulateBounds(this.upper, other.upper) + lowerBracket.join(upperBracket) + } catch (e: IllegalArgumentException) { + // Catch the exception if the operation is not defined and return TOP + log.warn("Modulo not defined for $this and $other: ${e.message}") + TOP + } + } + else -> { + log.warn("Unsupported interval type $this and $other") + TOP + } + } + } + + fun join(other: LatticeInterval): LatticeInterval { + // Join Operation + return when { + this is BOTTOM || other is BOTTOM -> BOTTOM + this is Bounded && other is Bounded -> { + val newLower = min(this.lower, other.lower) + val newUpper = max(this.upper, other.upper) + Bounded(newLower, newUpper) + } + else -> { + log.warn("Unsupported interval type $this and $other") + TOP + } + } + } + + fun meet(other: LatticeInterval): LatticeInterval { + // Meet Operation + return when { + this is BOTTOM -> other + other is BOTTOM -> this + // Check if the overlap at all + this.compareTo(other) != 0 -> BOTTOM + this is Bounded && other is Bounded -> { + val newLower = max(this.lower, other.lower) + val newUpper = min(this.upper, other.upper) + Bounded(newLower, newUpper) + } + else -> { + log.warn("Unsupported interval type $this and $other") + TOP + } + } + } + + fun widen(other: LatticeInterval): LatticeInterval { + // Widening + if (this !is Bounded) { + return other + } else if (other !is Bounded) { + return this + } + val lower: Bound = + when { + max(this.lower, other.lower) == other.lower -> { + this.lower + } + else -> Bound.NEGATIVE_INFINITE + } + val upper: Bound = + when { + max(this.upper, other.upper) == this.upper -> { + this.upper + } + else -> Bound.INFINITE + } + return Bounded(lower, upper) + } + + fun narrow(other: LatticeInterval): LatticeInterval { + // Narrowing + if (this !is Bounded || other !is Bounded) { + return BOTTOM + } + val lower: Bound = + when { + this.lower == Bound.NEGATIVE_INFINITE -> { + other.lower + } + else -> this.lower + } + val upper: Bound = + when { + this.upper == Bound.INFINITE -> { + other.upper + } + else -> this.upper + } + return Bounded(lower, upper) + } + + private fun min(one: Bound, other: Bound): Bound { + return when { + one is Bound.INFINITE || other is Bound.NEGATIVE_INFINITE -> other + other is Bound.INFINITE || one is Bound.NEGATIVE_INFINITE -> one + one is Bound.Value && other is Bound.Value -> + Bound.Value(kotlin.math.min(one.value, other.value)) + else -> throw IllegalArgumentException("Unsupported interval type $one and $other") + } + } + + private fun max(one: Bound, other: Bound): Bound { + return when { + one is Bound.INFINITE || other is Bound.NEGATIVE_INFINITE -> one + other is Bound.INFINITE || one is Bound.NEGATIVE_INFINITE -> other + one is Bound.Value && other is Bound.Value -> + Bound.Value(kotlin.math.max(one.value, other.value)) + else -> throw IllegalArgumentException("Unsupported interval type $one and $other") + } + } + + private fun addBounds(a: Bound, b: Bound, type: BoundType): Bound { + return when { + // -āˆž + āˆž is defined as -āˆž or as āˆž depending on the type + a is Bound.INFINITE && b !is Bound.NEGATIVE_INFINITE -> Bound.INFINITE + a is Bound.NEGATIVE_INFINITE && b is Bound.INFINITE -> + if (type == BoundType.LOWER) Bound.NEGATIVE_INFINITE else Bound.INFINITE + b is Bound.NEGATIVE_INFINITE && a is Bound.INFINITE -> + if (type == BoundType.LOWER) Bound.NEGATIVE_INFINITE else Bound.INFINITE + a is Bound.NEGATIVE_INFINITE && b !is Bound.INFINITE -> Bound.NEGATIVE_INFINITE + b is Bound.INFINITE && a !is Bound.NEGATIVE_INFINITE -> Bound.INFINITE + b is Bound.NEGATIVE_INFINITE && a !is Bound.INFINITE -> Bound.NEGATIVE_INFINITE + a is Bound.Value && b is Bound.Value -> Bound.Value(a.value + b.value) + else -> throw IllegalArgumentException("Unsupported bound type $a and $b") + } + } + + private fun subtractBounds(a: Bound, b: Bound): Bound { + return when { + // āˆž - āˆž is defined as āˆž + a is Bound.INFINITE && b is Bound.INFINITE -> Bound.INFINITE + a is Bound.INFINITE && b !is Bound.INFINITE -> Bound.INFINITE + a is Bound.NEGATIVE_INFINITE && b !is Bound.NEGATIVE_INFINITE -> Bound.NEGATIVE_INFINITE + b is Bound.INFINITE && a !is Bound.INFINITE -> Bound.NEGATIVE_INFINITE + b is Bound.NEGATIVE_INFINITE && a !is Bound.NEGATIVE_INFINITE -> Bound.INFINITE + a is Bound.Value && b is Bound.Value -> Bound.Value(a.value - b.value) + else -> throw IllegalArgumentException("Unsupported bound type $a and $b") + } + } + + private fun multiplyBounds(a: Bound, b: Bound): Bound { + return when { + // āˆž * 0 is defined as 0 + a is Bound.INFINITE && b == Bound.Value(0) -> Bound.Value(0) + a is Bound.NEGATIVE_INFINITE && b == Bound.Value(0) -> Bound.Value(0) + a is Bound.INFINITE && b > Bound.Value(0) -> Bound.INFINITE + a is Bound.INFINITE && b < Bound.Value(0) -> Bound.NEGATIVE_INFINITE + a > Bound.Value(0) && b is Bound.INFINITE -> Bound.INFINITE + a < Bound.Value(0) && b is Bound.INFINITE -> Bound.NEGATIVE_INFINITE + a is Bound.NEGATIVE_INFINITE && b > Bound.Value(0) -> Bound.NEGATIVE_INFINITE + a is Bound.NEGATIVE_INFINITE && b < Bound.Value(0) -> Bound.INFINITE + a > Bound.Value(0) && b is Bound.NEGATIVE_INFINITE -> Bound.NEGATIVE_INFINITE + a < Bound.Value(0) && b is Bound.NEGATIVE_INFINITE -> Bound.INFINITE + a is Bound.Value && b is Bound.Value -> Bound.Value(a.value * b.value) + else -> throw IllegalArgumentException("Unsupported bound type $a and $b") + } + } + + private fun divideBounds(a: Bound, b: Bound): Bound { + return when { + // āˆž / āˆž is not a defined operation + // x / 0 is not a defined operation + a is Bound.INFINITE && b > Bound.Value(0) && b !is Bound.INFINITE -> Bound.INFINITE + a is Bound.INFINITE && b < Bound.Value(0) && b !is Bound.NEGATIVE_INFINITE -> + Bound.NEGATIVE_INFINITE + a is Bound.NEGATIVE_INFINITE && b > Bound.Value(0) && b !is Bound.INFINITE -> + Bound.NEGATIVE_INFINITE + a is Bound.NEGATIVE_INFINITE && b < Bound.Value(0) && b !is Bound.NEGATIVE_INFINITE -> + Bound.INFINITE + // We estimate x / āˆž as 0 (with x != āˆž) + a !is Bound.NEGATIVE_INFINITE && + a !is Bound.INFINITE && + (b is Bound.NEGATIVE_INFINITE || b is Bound.INFINITE) -> Bound.Value(0) + a is Bound.Value && b is Bound.Value && b != Bound.Value(0) -> + Bound.Value(a.value / b.value) + else -> throw IllegalArgumentException("Unsupported bound type $a and $b") + } + } + + // āˆž mod b can be any number [0, b], therefore we need to return an Interval + private fun modulateBounds(a: Bound, b: Bound): LatticeInterval { + return when { + // x mod 0 is not a defined operation + // we approximate āˆž mod x as any number [0, b] (with x != āˆž) + // x mod -āˆž is not a defined operation + a == Bound.Value(0) -> Bounded(0, 0) + (a is Bound.INFINITE || a is Bound.NEGATIVE_INFINITE) && b != Bound.Value(0) -> + Bounded(0, b) + b is Bound.INFINITE -> Bounded(a, a) + a is Bound.Value && b is Bound.Value && b != Bound.Value(0) -> + Bounded(a.value % b.value, a.value % b.value) + else -> throw IllegalArgumentException("Unsupported bound type $a and $b") + } + } + + override fun toString(): String { + return when (this) { + is BOTTOM -> "BOTTOM" + is Bounded -> this.toString() + } + } + + override fun hashCode(): Int { + return javaClass.hashCode() + } +} + +enum class BoundType { + LOWER, + UPPER, +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValue.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValue.kt new file mode 100644 index 00000000000..3537614c4fa --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValue.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.AbstractIntervalEvaluator +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.analysis.abstracteval.TupleState +import de.fraunhofer.aisec.cpg.analysis.abstracteval.TupleStateElement +import de.fraunhofer.aisec.cpg.analysis.abstracteval.intervalOf +import de.fraunhofer.aisec.cpg.analysis.abstracteval.pushToDeclarationState +import de.fraunhofer.aisec.cpg.analysis.abstracteval.pushToGeneralState +import de.fraunhofer.aisec.cpg.evaluation.ValueEvaluator +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.edges.flows.EvaluationOrder +import de.fraunhofer.aisec.cpg.graph.statements.expressions.AssignExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.InitializerListExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.NewArrayExpression +import de.fraunhofer.aisec.cpg.graph.types.PointerType +import de.fraunhofer.aisec.cpg.query.size +import de.fraunhofer.aisec.cpg.query.value + +/** + * A [ValueEvaluator] which evaluates the size of arrays. It uses the [ArrayValue] class to track + * the size of the collection. + */ +class ArraySizeEvaluator : ValueEvaluator() { + override fun evaluate(node: Any?): Any? { + if (node !is Node) return cannotEvaluate(null, this) + + return AbstractIntervalEvaluator().evaluate(node, ArrayValue::class) + } +} + +/** + * This class implements the [Value] interface for Arrays, tracking the size of the collection. We + * assume that there is no operation that changes an array's size apart from re-declaring it. + */ +class ArrayValue : Value { + override fun applyEffect( + lattice: TupleState, + state: TupleStateElement, + node: Node, + edge: EvaluationOrder?, + computeWithoutPush: Boolean, + ): LatticeInterval { + var size: LatticeInterval = LatticeInterval.BOTTOM + var target: Node? = null + if (node is VariableDeclaration && node.initializer != null && node.type is PointerType) { + size = getSize(node.initializer!!) + target = node + } else if (node is AssignExpression && node.rhs.size == 1 && node.lhs.size == 1) { + size = getSize(node.rhs.single()) + target = node.lhs.single() + } + if (target != null) { + lattice.pushToGeneralState(state, target, size) + lattice.pushToDeclarationState(state, target, size) + lattice.pushToGeneralState(state, node, state.intervalOf(node)) + return size + } + + lattice.pushToGeneralState(state, node, state.intervalOf(node)) + return state.intervalOf(node) + } + + private fun getSize(node: Node): LatticeInterval { + return when (node) { + is Literal<*> -> { + if (node.value is String) { + // For strings, we return the length of the string + val length = (node.value as String).length.toLong() + LatticeInterval.Bounded(length, length) + } else { + // Otherwise, we assume that the size is 1. + LatticeInterval.Bounded(1, 1) + } + } + is InitializerListExpression -> { + // The number of elements in the initializer list + val length = node.initializers.size.toLong() + LatticeInterval.Bounded(length, length) + // node.initializers.fold(0L) { acc, init -> acc + getSize(init) } + } + is NewArrayExpression -> { + if (node.initializer != null) { + getSize(node.initializer!!) + } else { + val length = + node.dimensions + .map { (it.value.value as Number).toLong() } + .reduce { acc, dimension -> acc * dimension } + LatticeInterval.Bounded(length, length) + } + } + is CallExpression -> { + if (node.name.localName == "malloc") { + val length = (node.arguments.singleOrNull()?.value?.value as? Number)?.toLong() + length?.let { LatticeInterval.Bounded(length, length) } + ?: LatticeInterval.BOTTOM + } else { + LatticeInterval.BOTTOM + } + } + else -> TODO() + } + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValue.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValue.kt new file mode 100644 index 00000000000..ebea12f7219 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValue.kt @@ -0,0 +1,519 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.AbstractIntervalEvaluator +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.analysis.abstracteval.TupleState +import de.fraunhofer.aisec.cpg.analysis.abstracteval.TupleStateElement +import de.fraunhofer.aisec.cpg.analysis.abstracteval.changeDeclarationState +import de.fraunhofer.aisec.cpg.analysis.abstracteval.intervalOf +import de.fraunhofer.aisec.cpg.analysis.abstracteval.pushToDeclarationState +import de.fraunhofer.aisec.cpg.analysis.abstracteval.pushToGeneralState +import de.fraunhofer.aisec.cpg.evaluation.ValueEvaluator +import de.fraunhofer.aisec.cpg.graph.BranchingNode +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.edges.flows.EvaluationOrder +import de.fraunhofer.aisec.cpg.graph.statements.expressions.AssignExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference +import de.fraunhofer.aisec.cpg.graph.statements.expressions.UnaryOperator +import de.fraunhofer.aisec.cpg.graph.types.IntegerType +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * A [ValueEvaluator] which evaluates the possible integer values as a range of a [Node]. It uses + * the [IntegerValue] class to track possible values. + */ +class IntegerIntervalEvaluator : ValueEvaluator() { + override fun evaluate(node: Any?): Any? { + if (node !is Node) return cannotEvaluate(null, this) + + return AbstractIntervalEvaluator().evaluate(node, IntegerValue::class) + } +} + +/** This class implements the [Value] interface for Integer values. */ +class IntegerValue : Value { + + companion object { + val log: Logger = LoggerFactory.getLogger(IntegerValue::class.java) + } + + private fun simpleComparison( + lhs: Node, + rhs: Node, + operator: String?, + lattice: TupleState, + state: TupleStateElement, + ): TupleStateElement { + if (operator == "||" && lhs is BinaryOperator && rhs is BinaryOperator) { + val lhsState = state.duplicate() + simpleComparison(lhs.lhs, rhs.lhs, lhs.operatorCode, lattice, lhsState) + val rhsState = state.duplicate() + simpleComparison(rhs.lhs, rhs.lhs, rhs.operatorCode, lattice, rhsState) + val newState = lattice.lub(lhsState, rhsState, false) + // TODO: Handle the new state + return newState + } else if (operator == "&&" && lhs is BinaryOperator && rhs is BinaryOperator) { + val lhsState = state.duplicate() + simpleComparison(lhs.lhs, rhs.lhs, lhs.operatorCode, lattice, lhsState) + val rhsState = state.duplicate() + simpleComparison(rhs.lhs, rhs.lhs, rhs.operatorCode, lattice, rhsState) + val newState = lattice.glb(lhsState, rhsState) + // TODO: Handle the new state + return newState + } + + val lhsValue = state.intervalOf(lhs) + val rhsValue = state.intervalOf(rhs) + + if ( + lhs is Reference && + lhsValue is LatticeInterval.Bounded && + rhsValue is LatticeInterval.Bounded + ) { + val newLhsInterval = + when (operator) { + "<" -> { + val newUpper = + minOf(lhsValue.upper, rhsValue.upper - LatticeInterval.Bound.Value(1)) + if (newUpper < lhsValue.lower) { + LatticeInterval.BOTTOM + } else { + LatticeInterval.Bounded(lhsValue.lower, newUpper) + } + } + "<=" -> { + val newUpper = minOf(lhsValue.upper, rhsValue.upper) + if (newUpper < lhsValue.lower) { + LatticeInterval.BOTTOM + } else { + LatticeInterval.Bounded(lhsValue.lower, newUpper) + } + } + ">" -> { + val newLower = + maxOf(lhsValue.lower, rhsValue.lower + LatticeInterval.Bound.Value(1)) + if (newLower > lhsValue.upper) { + LatticeInterval.BOTTOM + } else { + LatticeInterval.Bounded(newLower, lhsValue.upper) + } + } + ">=" -> { + val newLower = maxOf(lhsValue.lower, rhsValue.lower) + if (newLower > lhsValue.upper) { + LatticeInterval.BOTTOM + } else { + LatticeInterval.Bounded(newLower, lhsValue.upper) + } + } + "==" -> { + val newLower = maxOf(lhsValue.lower, rhsValue.lower) + val newUpper = minOf(lhsValue.upper, rhsValue.upper) + if (newLower > newUpper) { + LatticeInterval.BOTTOM + } else { + LatticeInterval.Bounded(newLower, newUpper) + } + } + "!=" -> { + if (rhsValue.lower == rhsValue.upper) { + if (lhsValue.lower == rhsValue.upper) { + // Move lower bound up by one + LatticeInterval.Bounded( + lhsValue.lower + LatticeInterval.Bound.Value(1), + lhsValue.upper, + ) + } else if (lhsValue.upper == rhsValue.lower) { + // Move upper bound up by one + LatticeInterval.Bounded( + lhsValue.lower, + lhsValue.upper - LatticeInterval.Bound.Value(1), + ) + } else { + lhsValue + } + } else { + lhsValue + } + } + else -> { + lhsValue + } + } + lattice.changeDeclarationState(state, lhs, newLhsInterval) + lattice.pushToGeneralState(state, lhs, newLhsInterval) + } + return state + } + + override fun applyEffect( + lattice: TupleState, + state: TupleStateElement, + node: Node, + edge: EvaluationOrder?, + computeWithoutPush: Boolean, + ): LatticeInterval { + val prevNode = edge?.start + // For a node after a branching node: Calculate how variables are affected by the condition + // in the different branches. + val state = + if (prevNode is BranchingNode) { + val condition = prevNode.branchedBy + if (edge.branch == true && condition is BinaryOperator) { + // The "then" branch is taken, so the condition is true, and we can use the + // condition as is. + simpleComparison( + condition.lhs, + condition.rhs, + condition.operatorCode, + lattice, + state, + ) + } else if (edge.branch == false && condition is BinaryOperator) { + // The "else" branch is taken, so the condition is false, and we have to + // calculate the negation of the condition. + val invertedOperator = + when (condition.operatorCode) { + "==" -> "!=" + "!=" -> "==" + "<" -> ">=" + "<=" -> ">" + ">" -> "<=" + ">=" -> "<" + else -> condition.operatorCode + } + simpleComparison(condition.lhs, condition.rhs, invertedOperator, lattice, state) + } else { + // Unknown branch. Continue with the current state. + state + } + } else { + state + } + + if (node is Literal<*>) { + val value = node.value as? Number ?: return state.intervalOf(node) + + val interval = LatticeInterval.Bounded(value.toLong(), value.toLong()) + if (!computeWithoutPush) { + lattice.pushToDeclarationState(state, node, interval) + lattice.pushToGeneralState(state, node, interval) + } + return interval + } // (Re-)Declarations of the Variable + else if (node is VariableDeclaration) { + val initializerValue = + node.initializer?.let { + this.applyEffect(lattice, state, it, null, computeWithoutPush = true) + } ?: LatticeInterval.TOP + lattice.pushToDeclarationState(state, node, initializerValue) + lattice.pushToGeneralState(state, node, initializerValue) + return initializerValue + } + // Unary Operators + else if (node is UnaryOperator) { + val current = state.intervalOf(node.input) + val newValue = + when (node.operatorCode) { + "++" -> { + val oneInterval = LatticeInterval.Bounded(1, 1) + current + oneInterval + } + "--" -> { + val oneInterval = LatticeInterval.Bounded(1, 1) + current - oneInterval + } + else -> current + } + lattice.changeDeclarationState(state, node.input, newValue) + lattice.pushToGeneralState(state, node, newValue) + return newValue + } // Binary Operators + else if (node is BinaryOperator) { + val lhsValue = state.intervalOf(node.lhs) + val rhsValue = state.intervalOf(node.rhs) + val newValue = + when (node.operatorCode) { + "+" -> { + lhsValue + rhsValue + } + "-" -> { + lhsValue - rhsValue + } + "*" -> { + lhsValue * rhsValue + } + "/" -> { + lhsValue / rhsValue + } + "%" -> { + lhsValue % rhsValue + } + + "<" -> { + if ( + lhsValue is LatticeInterval.Bounded && + rhsValue is LatticeInterval.Bounded + ) { + if (lhsValue.lower > rhsValue.upper) { + LatticeInterval.Bounded(0, 0) + } else if (lhsValue.upper < rhsValue.lower) { + LatticeInterval.Bounded(1, 1) + } else { + LatticeInterval.Bounded(0, 1) + } + } else { + LatticeInterval.TOP // Cannot determine bounds + } + } + "<=" -> { + if ( + lhsValue is LatticeInterval.Bounded && + rhsValue is LatticeInterval.Bounded + ) { + if (lhsValue.lower >= rhsValue.upper) { + LatticeInterval.Bounded(0, 0) + } else if (lhsValue.upper <= rhsValue.lower) { + LatticeInterval.Bounded(1, 1) + } else { + LatticeInterval.Bounded(0, 1) + } + } else { + LatticeInterval.TOP // Cannot determine bounds + } + } + ">" -> { + if ( + lhsValue is LatticeInterval.Bounded && + rhsValue is LatticeInterval.Bounded + ) { + if (lhsValue.lower < rhsValue.upper) { + LatticeInterval.Bounded(0, 0) + } else if (lhsValue.upper > rhsValue.lower) { + LatticeInterval.Bounded(1, 1) + } else { + LatticeInterval.Bounded(0, 1) + } + } else { + LatticeInterval.TOP // Cannot determine bounds + } + } + ">=" -> { + if ( + lhsValue is LatticeInterval.Bounded && + rhsValue is LatticeInterval.Bounded + ) { + if (lhsValue.lower <= rhsValue.upper) { + LatticeInterval.Bounded(0, 0) + } else if (lhsValue.upper >= rhsValue.lower) { + LatticeInterval.Bounded(1, 1) + } else { + LatticeInterval.Bounded(0, 1) + } + } else { + LatticeInterval.TOP // Cannot determine bounds + } + } + "==" -> { + if ( + lhsValue is LatticeInterval.Bounded && + rhsValue is LatticeInterval.Bounded + ) { + if ( + lhsValue.upper == lhsValue.lower && + rhsValue.lower == rhsValue.upper && + lhsValue.lower == rhsValue.lower + ) { + LatticeInterval.Bounded(1, 1) + } else if ( + lhsValue.upper < rhsValue.lower || rhsValue.upper < lhsValue.lower + ) { + LatticeInterval.Bounded(0, 0) + } else { + LatticeInterval.Bounded(0, 1) + } + } else { + LatticeInterval.TOP // Cannot determine bounds + } + } + "!=" -> { + if ( + lhsValue is LatticeInterval.Bounded && + rhsValue is LatticeInterval.Bounded + ) { + if ( + lhsValue.upper == lhsValue.lower && + rhsValue.lower == rhsValue.upper && + lhsValue.lower == rhsValue.lower + ) { + LatticeInterval.Bounded(0, 0) + } else if ( + lhsValue.upper < rhsValue.lower || rhsValue.upper < lhsValue.lower + ) { + LatticeInterval.Bounded(1, 1) + } else { + LatticeInterval.Bounded(0, 1) + } + } else { + LatticeInterval.TOP // Cannot determine bounds + } + } + "<<" -> { + // We need to know how many bits our value can have as this affects the + // shift's outcome. If we do not know, we assume 32 bits. + // TODO: Configure this better, e.g. based on the platform if we have + // information about it. + lhsValue.shl( + rhsValue, + maxBits = (node.type as? IntegerType)?.bitWidth ?: 32, + ) + } + ">>" -> { + // We need to know how many bits our value can have as this affects the + // shift's outcome. If we do not know, we assume 32 bits. + // TODO: Configure this better, e.g. based on the platform if we have + // information about it. + lhsValue.shr( + rhsValue, + maxBits = (node.type as? IntegerType)?.bitWidth ?: 32, + ) + } + "|" -> { + lhsValue.bitwiseOr(rhsValue) + } + "^" -> { + // TODO: We can do better here, but for now we just return TOP + LatticeInterval.TOP + } + "&" -> { + lhsValue.bitwiseAnd(rhsValue) + } + "&&" -> { + if ( + lhsValue is LatticeInterval.Bounded && + rhsValue is LatticeInterval.Bounded + ) { + if ( + lhsValue.lower == LatticeInterval.Bound.Value(0L) && + lhsValue.upper == LatticeInterval.Bound.Value(0L) || + rhsValue.lower == LatticeInterval.Bound.Value(0L) && + rhsValue.upper == LatticeInterval.Bound.Value(0L) + ) { + // One value is always zero, so the result is zero + LatticeInterval.Bounded(0, 0) + } else if ( + lhsValue.lower <= LatticeInterval.Bound.Value(0L) && + lhsValue.upper >= LatticeInterval.Bound.Value(0L) || + rhsValue.lower <= LatticeInterval.Bound.Value(0L) && + rhsValue.upper >= LatticeInterval.Bound.Value(0L) + ) { + // One of the values can be zero, so the result is unknown + LatticeInterval.Bounded(0, 1) + } else { + // Both values are non-zero, so the result is 1 + LatticeInterval.Bounded(1, 1) + } + } else { + LatticeInterval.TOP // Cannot determine bounds + } + } + "||" -> { + if ( + lhsValue is LatticeInterval.Bounded && + rhsValue is LatticeInterval.Bounded + ) { + if ( + lhsValue.lower > LatticeInterval.Bound.Value(0L) || + rhsValue.lower > LatticeInterval.Bound.Value(0L) || + lhsValue.upper < LatticeInterval.Bound.Value(0L) || + rhsValue.upper < LatticeInterval.Bound.Value(0L) + ) { + // One of the values is always non-zero, so the result is 1 + LatticeInterval.Bounded(1, 1) + } else if ( + lhsValue.lower == LatticeInterval.Bound.Value(0L) && + lhsValue.upper == LatticeInterval.Bound.Value(0L) && + rhsValue.lower == LatticeInterval.Bound.Value(0L) && + rhsValue.upper == LatticeInterval.Bound.Value(0L) + ) { + // Both of the values is always zero, so the result is 0 + LatticeInterval.Bounded(0, 0) + } else { + LatticeInterval.Bounded(0, 1) + } + } else { + LatticeInterval.TOP // Cannot determine bounds + } + } + else -> { + log.info("Unsupported binary operator: ${node.operatorCode}") + LatticeInterval.TOP // Cannot determine bounds + } + } + lattice.pushToGeneralState(state, node, newValue) + lattice.pushToDeclarationState(state, node, newValue) + return newValue + } + // Assignments and combined assign expressions + else if (node is AssignExpression) { + if (node.lhs.size == 1 && node.rhs.size == 1) { + // The lhs and rhs must already have been evaluated before reaching the operator. + // This should be guaranteed by the evaluation order graph. + val rhsValue = state.intervalOf(node.rhs[0]) + val lhsValue = state.intervalOf(node.lhs[0]) + val newValue = + when (node.operatorCode) { + "=" -> rhsValue + "+=" -> lhsValue + rhsValue + "-=" -> lhsValue - rhsValue + "*=" -> lhsValue * rhsValue + "/=" -> lhsValue / rhsValue + "%=" -> lhsValue % rhsValue + else -> { + log.info("Unsupported assignment operator: ${node.operatorCode}") + LatticeInterval.TOP + } + } + // Push the new value to the declaration state of the variable + lattice.changeDeclarationState(state, node.lhs.first(), newValue) + // lattice.pushToGeneralState(state, node, newValue) + return newValue + } else { + // We do not support multiple lhs or rhs in the current implementation. + } + } + + lattice.pushToGeneralState(state, node, state.intervalOf(node)) + + return state.intervalOf(node) + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableCollectionSize.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableCollectionSize.kt new file mode 100644 index 00000000000..ba62b43945a --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableCollectionSize.kt @@ -0,0 +1,162 @@ +/* + * 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.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.analysis.abstracteval.TupleStateElement +import de.fraunhofer.aisec.cpg.analysis.abstracteval.intervalOf +import de.fraunhofer.aisec.cpg.graph.Node + +/** + * This class implements the [Value] interface for tracking the size of mutable collections. It + * provides several operations which can then be used for concrete implementations. + */ +abstract class MutableCollectionSize : Value { + fun createEmptyCollection(): LatticeInterval { + return LatticeInterval.Bounded(0, 0) + } + + fun createWithElementsFromElement( + element: Node, + state: TupleStateElement, + ): LatticeInterval { + return state.intervalOf(element) + } + + fun createWithElements(elements: List, state: TupleStateElement): LatticeInterval { + return LatticeInterval.Bounded(elements.size, elements.size) + } + + fun addSingleElementWithoutElementCheck( + target: Node, + state: TupleStateElement, + ): LatticeInterval { + return state.intervalOf(target) + LatticeInterval.Bounded(1, 1) + } + + fun addSingleElementWithElementCheck( + target: Node, + state: TupleStateElement, + ): LatticeInterval { + return state.intervalOf(target) + LatticeInterval.Bounded(0, 1) + } + + fun addMultipleElementsWithoutElementCheck( + target: Node, + element: Node, + state: TupleStateElement, + ): LatticeInterval { + return state.intervalOf(target) + state.intervalOf(element) + } + + fun addMultipleElementsWithElementCheck( + target: Node, + element: Node, + state: TupleStateElement, + ): LatticeInterval { + val elementSize = state.intervalOf(element) + // TODO: Do we want infinite or zero as upper bound or return bottom if we have bottom for + // the element? + val maxSize = + (elementSize as? LatticeInterval.Bounded)?.upper ?: LatticeInterval.Bound.INFINITE + return state.intervalOf(target) + + LatticeInterval.Bounded(LatticeInterval.Bound.Value(0), maxSize) + } + + fun addMultipleElementsWithoutElementCheck( + target: Node, + elements: List, + state: TupleStateElement, + ): LatticeInterval { + return state.intervalOf(target) + LatticeInterval.Bounded(elements.size, elements.size) + } + + fun addMultipleElementsWithElementCheck( + target: Node, + elements: List, + state: TupleStateElement, + ): LatticeInterval { + return state.intervalOf(target) + LatticeInterval.Bounded(0, elements.size) + } + + fun clearAllElements(): LatticeInterval { + return LatticeInterval.Bounded(0, 0) + } + + fun removeSingleElementWithoutElementCheck( + target: Node, + state: TupleStateElement, + ): LatticeInterval { + val result = state.intervalOf(target) - LatticeInterval.Bounded(1, 1) + + // If the lower bound is less than 0, we set it to 0 as negative sizes do not make sense + if (result is LatticeInterval.Bounded && result.lower < LatticeInterval.Bound.Value(0)) { + return LatticeInterval.Bounded(LatticeInterval.Bound.Value(0), result.upper) + } + return result + } + + fun removeSingleElementWithElementCheck( + target: Node, + state: TupleStateElement, + ): LatticeInterval { + val result = state.intervalOf(target) - LatticeInterval.Bounded(0, 1) + + // If the lower bound is less than 0, we set it to 0 as negative sizes do not make sense + if (result is LatticeInterval.Bounded && result.lower < LatticeInterval.Bound.Value(0)) { + return LatticeInterval.Bounded(LatticeInterval.Bound.Value(0), result.upper) + } + return result + } + + fun removeMultipleElementsWithoutElementCheck( + target: Node, + state: TupleStateElement, + ): LatticeInterval { + // We could remove all elements if all are similar, so we only know that the new size will + // be somewhere between 0 and the current maximal size + val result = state.intervalOf(target) + + if (result is LatticeInterval.Bounded) { + return LatticeInterval.Bounded(LatticeInterval.Bound.Value(0), result.upper) + } + return result + } + + fun removeMultipleElementsWithElementCheck( + target: Node, + elements: List, + state: TupleStateElement, + ): LatticeInterval { + val result = state.intervalOf(target) - LatticeInterval.Bounded(0, elements.size) + + // If the lower bound is less than 0, we set it to 0 as negative sizes do not make sense + if (result is LatticeInterval.Bounded && result.lower < LatticeInterval.Bound.Value(0)) { + return LatticeInterval.Bounded(LatticeInterval.Bound.Value(0), result.upper) + } + return result + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListSize.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListSize.kt new file mode 100644 index 00000000000..b8001f09dd8 --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListSize.kt @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.AbstractIntervalEvaluator +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.analysis.abstracteval.TupleState +import de.fraunhofer.aisec.cpg.analysis.abstracteval.TupleStateElement +import de.fraunhofer.aisec.cpg.analysis.abstracteval.pushToDeclarationState +import de.fraunhofer.aisec.cpg.analysis.abstracteval.pushToGeneralState +import de.fraunhofer.aisec.cpg.evaluation.ValueEvaluator +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.edges.flows.EvaluationOrder +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.NewExpression +import de.fraunhofer.aisec.cpg.graph.types.IntegerType +import de.fraunhofer.aisec.cpg.graph.types.ListType + +/** + * A [ValueEvaluator] which evaluates the size of mutable lists. It uses the [MutableSetSize] class + * to track the size of the collection. + */ +class ListSizeEvaluator : ValueEvaluator() { + override fun evaluate(node: Any?): Any? { + if (node !is Node) return cannotEvaluate(null, this) + + return AbstractIntervalEvaluator().evaluate(node, MutableListSize::class) + } +} + +/** + * This class implements the [Value] interface for mutable Lists, tracking the size of the + * collection. We assume that there is no operation that changes an array's size apart from + * re-declaring it. NOTE: This is an unpolished example implementation. Before actual usage consider + * the below TODOs and write a test file. + */ +class MutableListSize() : MutableCollectionSize() { + override fun applyEffect( + lattice: TupleState, + state: TupleStateElement, + node: Node, + edge: EvaluationOrder?, + computeWithoutPush: Boolean, + ): LatticeInterval { + var target: Node? = null + var variableSize: LatticeInterval = LatticeInterval.BOTTOM + + if (node is VariableDeclaration && node.initializer != null) { + target = node + variableSize = + when (val init = node.initializer) { + is MemberCallExpression -> { + if (init.base?.type is ListType) { // TODO: check the function name + // This is a List copying the base, we can use the size of the base + createWithElementsFromElement(init.base!!, state) + } else { + val size = init.arguments.size.toLong() + LatticeInterval.Bounded(size, size) + } + } + is NewExpression -> { + if ( + init.initializer !is CallExpression || + (init.initializer as CallExpression).arguments.isEmpty() + ) { + // Empty List, we can use the empty collection size + createEmptyCollection() + } else if ( + (init.initializer as CallExpression).arguments.size == 1 && + (init.initializer as CallExpression).arguments.single().type is + ListType + ) { + // This is a List with a single element, we can use the size of that + // element + val element = (init.initializer as CallExpression).arguments.single() + createWithElementsFromElement(element, state) + } else { + // This is a List with multiple elements, we can use the size of those + // elements + val elements = (init.initializer as CallExpression).arguments + createWithElements(elements, state) + } + } + else -> LatticeInterval.BOTTOM + } + } else if (node is MemberCallExpression && node.base != null) { + target = node.base!! + + variableSize = + when (node.name.localName) { + "add" -> { + addSingleElementWithoutElementCheck(target, state) + } + + "addAll" -> { + if (node.arguments.singleOrNull()?.type is ListType) { + addMultipleElementsWithoutElementCheck( + target, + node.arguments.single(), + state, + ) + } else { + addMultipleElementsWithoutElementCheck(target, node.arguments, state) + } + } + + "clear" -> { + clearAllElements() + } + + "remove" -> { + // We have to differentiate between remove with index or object argument + // Latter may do nothing if the element is not in the list + if (node.arguments.first().type is IntegerType) { + removeSingleElementWithoutElementCheck(target, state) + } else { + removeSingleElementWithElementCheck(target, state) + } + } + + "removeAll" -> { + removeMultipleElementsWithoutElementCheck(target, state) + } + + else -> LatticeInterval.BOTTOM + } + } + + if (target != null) { + lattice.pushToGeneralState(state, target, variableSize) + lattice.pushToDeclarationState(state, target, variableSize) + } + lattice.pushToDeclarationState(state, node, LatticeInterval.BOTTOM) + + // TODO: Push stuff, modify state. + return variableSize + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableSetSize.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableSetSize.kt new file mode 100644 index 00000000000..3341abc6b7f --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableSetSize.kt @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.AbstractIntervalEvaluator +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.analysis.abstracteval.TupleState +import de.fraunhofer.aisec.cpg.analysis.abstracteval.TupleStateElement +import de.fraunhofer.aisec.cpg.analysis.abstracteval.pushToDeclarationState +import de.fraunhofer.aisec.cpg.analysis.abstracteval.pushToGeneralState +import de.fraunhofer.aisec.cpg.evaluation.ValueEvaluator +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.edges.flows.EvaluationOrder +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.NewExpression +import de.fraunhofer.aisec.cpg.graph.types.ListType +import de.fraunhofer.aisec.cpg.graph.types.SetType + +/** + * A [ValueEvaluator] which evaluates the size of mutable sets. It uses the [MutableSetSize] class + * to track the size of the collection. + */ +class SetSizeEvaluator : ValueEvaluator() { + override fun evaluate(node: Any?): Any? { + if (node !is Node) return cannotEvaluate(null, this) + + return AbstractIntervalEvaluator().evaluate(node, MutableSetSize::class) + } +} + +/** + * This class implements the [Value] interface for mutable Lists, tracking the size of the + * collection. We assume that there is no operation that changes an array's size apart from + * re-declaring it. NOTE: This is an unpolished example implementation. Before actual usage consider + * the below TODOs and write a test file. + */ +class MutableSetSize() : MutableCollectionSize() { + override fun applyEffect( + lattice: TupleState, + state: TupleStateElement, + node: Node, + edge: EvaluationOrder?, + computeWithoutPush: Boolean, + ): LatticeInterval { + var target: Node? = null + var variableSize: LatticeInterval = LatticeInterval.BOTTOM + + if (node is VariableDeclaration && node.initializer != null) { + target = node + variableSize = + when (val init = node.initializer) { + is MemberCallExpression -> { + if (init.base?.type is SetType) { // TODO: check the function name + // This is a set copying the base, we can use the size of the base + createWithElementsFromElement(init.base!!, state) + } else if (init.base?.type is ListType) { // TODO: check the function name + // This is a set copying the base but as set. This may remove some + // elements! + val tmp = createWithElementsFromElement(init.base!!, state) + if ( + tmp is LatticeInterval.Bounded && + tmp.upper > LatticeInterval.Bound.Value(0) + ) { + LatticeInterval.Bounded(LatticeInterval.Bound.Value(1), tmp.upper) + } else { + LatticeInterval.Bounded(0, 0) + } + } else { + val size = init.arguments.size.toLong() + if (size > 0) { + LatticeInterval.Bounded(1, size) + } else { + LatticeInterval.Bounded(0, 0) + } + } + } + is NewExpression -> { + if ( + init.initializer !is CallExpression || + (init.initializer as CallExpression).arguments.isEmpty() + ) { + // Empty List, we can use the empty collection size + createEmptyCollection() + } else if ( + (init.initializer as CallExpression).arguments.size == 1 && + (init.initializer as CallExpression).arguments.single().type is + ListType + ) { + // This is a List with a single element, we can use the size of that + // element + val element = (init.initializer as CallExpression).arguments.single() + createWithElementsFromElement(element, state) + } else { + // This is a List with multiple elements, we can use the size of those + // elements + val elements = (init.initializer as CallExpression).arguments + createWithElements(elements, state) + } + } + else -> LatticeInterval.BOTTOM + } + } else if (node is MemberCallExpression && node.base != null) { + target = node.base!! + + variableSize = + when (node.name.localName) { + "add" -> { + addSingleElementWithElementCheck(target, state) + } + + "addAll" -> { + if (node.arguments.singleOrNull()?.type is ListType) { + addMultipleElementsWithElementCheck( + target, + node.arguments.single(), + state, + ) + } else { + addMultipleElementsWithElementCheck(target, node.arguments, state) + } + } + + "clear" -> { + clearAllElements() + } + + "remove" -> { + removeSingleElementWithElementCheck(target, state) + } + + "removeAll" -> { + removeMultipleElementsWithoutElementCheck(target, state) + } + + else -> LatticeInterval.BOTTOM + } + } + + if (target != null) { + lattice.pushToGeneralState(state, target, variableSize) + lattice.pushToDeclarationState(state, target, variableSize) + } + lattice.pushToDeclarationState(state, node, LatticeInterval.BOTTOM) + + // TODO: Push stuff, modify state. + return variableSize + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/Value.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/Value.kt new file mode 100644 index 00000000000..ed6e43f775b --- /dev/null +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/Value.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.TupleState +import de.fraunhofer.aisec.cpg.analysis.abstracteval.TupleStateElement +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.edges.flows.EvaluationOrder +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference + +/** + * The [Value] interface is used by the AbstractEvaluator to store the behaviour of different + * analysis targets. Each class implementing this interface is expected to define all operations + * that might affect its internal value in [applyEffect]. When adding new classes remember to add + * them to AbstractEvaluator.getType and add tests. + */ +interface Value { + /** Applies the effect of a Node to the interval containing its possible values. */ + fun applyEffect( + lattice: TupleState, + state: TupleStateElement, + node: Node, + edge: EvaluationOrder? = null, + computeWithoutPush: Boolean = false, + ): T + + companion object { + fun getInitializer(node: Node?): Node? { + return when (node) { + null -> null + is Reference -> getInitializer(node.refersTo) + is VariableDeclaration -> node + else -> getInitializer(node.prevDFG.firstOrNull()) + } + } + } +} diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/UnreachableEOGPass.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/UnreachableEOGPass.kt index 971633f1559..c9f932be2f1 100644 --- a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/UnreachableEOGPass.kt +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/UnreachableEOGPass.kt @@ -292,7 +292,7 @@ class ReachabilityLattice() : Lattice { override val bottom: Element get() = Element(Reachability.BOTTOM) - override fun lub(one: Element, two: Element, allowModify: Boolean): Element { + override fun lub(one: Element, two: Element, allowModify: Boolean, widen: Boolean): Element { return if (allowModify) { when (compare(one, two)) { Order.EQUAL -> one diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Query.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Query.kt index cac54d06eaf..60e1d776b4a 100644 --- a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Query.kt +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/Query.kt @@ -25,8 +25,9 @@ */ package de.fraunhofer.aisec.cpg.query +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.analysis.abstracteval.value.IntegerIntervalEvaluator import de.fraunhofer.aisec.cpg.assumptions.addAssumptionDependence -import de.fraunhofer.aisec.cpg.evaluation.MultiValueEvaluator import de.fraunhofer.aisec.cpg.evaluation.NumberSet import de.fraunhofer.aisec.cpg.evaluation.SizeEvaluator import de.fraunhofer.aisec.cpg.evaluation.ValueEvaluator @@ -144,9 +145,19 @@ fun sizeof(n: Node?, eval: ValueEvaluator = SizeEvaluator()): QueryTree { * * @eval can be used to specify the evaluator but this method has to interpret the result correctly! */ -fun min(n: Node?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree { +fun min(n: Node?, eval: ValueEvaluator = IntegerIntervalEvaluator()): QueryTree { val evalRes = eval.evaluate(n) - if (evalRes is Number) { + if (evalRes is LatticeInterval) { + val result = + ((evalRes as? LatticeInterval.Bounded)?.upper as? LatticeInterval.Bound.Value)?.value + ?: Long.MIN_VALUE + return QueryTree( + result, + mutableListOf(QueryTree(n, operator = GenericQueryOperators.EVALUATE)), + node = n, + operator = GenericQueryOperators.EVALUATE, + ) + } else if (evalRes is Number) { return QueryTree( evalRes, mutableListOf(QueryTree(n, operator = GenericQueryOperators.EVALUATE)), @@ -170,7 +181,7 @@ fun min(n: Node?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree { +fun min(n: List?, eval: ValueEvaluator = IntegerIntervalEvaluator()): QueryTree { var result = Long.MAX_VALUE if (n == null) return QueryTree( @@ -181,10 +192,27 @@ fun min(n: List?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree for (node in n) { val evalRes = eval.evaluate(node) - if (evalRes is Number && evalRes.toLong() < result) { - result = evalRes.toLong() - } else if (evalRes is NumberSet && evalRes.min() < result) { - result = evalRes.min() + when (evalRes) { + is LatticeInterval -> { + val minValue = + ((evalRes as? LatticeInterval.Bounded)?.upper as? LatticeInterval.Bound.Value) + ?.value + ?: ((evalRes as? LatticeInterval.Bounded)?.upper + as? LatticeInterval.Bound.INFINITE) + ?.let { Long.MIN_VALUE } + ?: Long.MAX_VALUE + if (minValue < result) { + result = minValue + } + } + + is Number if evalRes.toLong() < result -> { + result = evalRes.toLong() + } + + is NumberSet if evalRes.min() < result -> { + result = evalRes.min() + } } // Extend this when we have other evaluators. } @@ -196,7 +224,7 @@ fun min(n: List?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree * * @eval can be used to specify the evaluator but this method has to interpret the result correctly! */ -fun max(n: List?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree { +fun max(n: List?, eval: ValueEvaluator = IntegerIntervalEvaluator()): QueryTree { var result = Long.MIN_VALUE if (n == null) return QueryTree( @@ -207,10 +235,27 @@ fun max(n: List?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree for (node in n) { val evalRes = eval.evaluate(node) - if (evalRes is Number && evalRes.toLong() > result) { - result = evalRes.toLong() - } else if (evalRes is NumberSet && evalRes.max() > result) { - result = evalRes.max() + when (evalRes) { + is LatticeInterval -> { + val maxValue = + ((evalRes as? LatticeInterval.Bounded)?.upper as? LatticeInterval.Bound.Value) + ?.value + ?: ((evalRes as? LatticeInterval.Bounded)?.upper + as? LatticeInterval.Bound.INFINITE) + ?.let { Long.MAX_VALUE } + ?: Long.MIN_VALUE + if (maxValue > result) { + result = maxValue + } + } + + is Number if evalRes.toLong() > result -> { + result = evalRes.toLong() + } + + is NumberSet if evalRes.max() > result -> { + result = evalRes.max() + } } // Extend this when we have other evaluators. } @@ -222,9 +267,20 @@ fun max(n: List?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree * * @eval can be used to specify the evaluator but this method has to interpret the result correctly! */ -fun max(n: Node?, eval: ValueEvaluator = MultiValueEvaluator()): QueryTree { +fun max(n: Node?, eval: ValueEvaluator = IntegerIntervalEvaluator()): QueryTree { val evalRes = eval.evaluate(n) - if (evalRes is Number) { + + if (evalRes is LatticeInterval) { + val result = + ((evalRes as? LatticeInterval.Bounded)?.upper as? LatticeInterval.Bound.Value)?.value + ?: Long.MAX_VALUE + return QueryTree( + result, + mutableListOf(QueryTree(n, operator = GenericQueryOperators.EVALUATE)), + node = n, + operator = GenericQueryOperators.EVALUATE, + ) + } else if (evalRes is Number) { return QueryTree( evalRes, mutableListOf(QueryTree(n, operator = GenericQueryOperators.EVALUATE)), diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractEvaluatorTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractEvaluatorTest.kt new file mode 100644 index 00000000000..bd2d75f9f7b --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractEvaluatorTest.kt @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.value.IntegerIntervalEvaluator +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration +import de.fraunhofer.aisec.cpg.testcases.AbstractEvaluationTests +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AbstractEvaluatorTest { + private lateinit var tu: TranslationUnitDeclaration + + @BeforeAll + fun beforeAll() { + tu = AbstractEvaluationTests.getIntegerExample().components.first().translationUnits.first() + } + + /* + Bar f = new Bar(); + int a = 5; + + a = 0; + a -= 2; + a += 3; + + b.f(a); + */ + @Test + fun testSimpleInteger() { + val mainClass = tu.records["Foo"] + assertNotNull(mainClass) + val f1 = mainClass.methods["f1"] + assertNotNull(f1, "There should be an argument for the call to f") + + val refA = f1.mcalls["f"]?.arguments?.singleOrNull() + assertNotNull(refA) + + val value = refA.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(1, 1), value) + } + + /* + Bar f = new Bar(); + int a = 5; + + a = 3; + a++; + ++a; + b.c(a); + a -= 2; + a += 3; + a--; + --a; + a *= 4; + a /= 2; + a %= 3; + + b.f(a); + */ + @Test + fun testIntegerOperations() { + val mainClass = tu.records["Foo"] + assertNotNull(mainClass) + val f2 = mainClass.methods["f2"] + assertNotNull(f2) + + val refAMiddle = f2.mcalls["c"]?.arguments?.singleOrNull() + assertNotNull(refAMiddle, "There should be an argument for the call to c") + + val valueMiddle = refAMiddle.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(5, 5), valueMiddle) + + val refAEnd = f2.mcalls["f"]?.arguments?.singleOrNull() + assertNotNull(refAEnd, "There should be an argument for the call to f") + + val valueEnd = refAEnd.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(2, 2), valueEnd) + } + + /* + Bar b = new Bar(); + int a = 5; + + if (new Random().nextBoolean()) { + a -= 1; + } + + b.f(a); + */ + @Test + fun testBranch1Integer() { + val mainClass = tu.records["Foo"] + assertNotNull(mainClass) + val f3 = mainClass.methods["f3"] + assertNotNull(f3) + + val declA = f3.variables["a"] + assertNotNull(declA, "There should be a variable declaration for a") + + val valueDecl = declA.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(5, 5), valueDecl) + + val refA = f3.mcalls["f"]?.arguments?.firstOrNull() + assertNotNull(refA, "There should be an argument for the call to f") + + val value = refA.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(4, 5), value) + } + + /* + Bar b = new Bar(); + int a = 5; + + if (new Random().nextBoolean()) { + a -= 1; + } else { + a = 3; + } + + b.f(a); + */ + @Test + fun testBranch2Integer() { + val mainClass = tu.records["Foo"] + assertNotNull(mainClass) + val f4 = mainClass.methods["f4"] + assertNotNull(f4) + + val refA = f4.mcalls["f"]?.arguments?.firstOrNull() + assertNotNull(refA, "There should be an argument for the call to f") + + val value = refA.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(3, 4), value) + } + + /* + Bar b = new Bar(); + int a = 5; + + for (int i = 0; i < 5; i++) { + a += 1; + println(i); + } + + b.f(a); + */ + @Test + fun testLoopInteger() { + val mainClass = tu.records["Foo"] + assertNotNull(mainClass) + val f5 = mainClass.methods["f5"] + assertNotNull(f5) + + val refI = f5.calls["println"]?.arguments?.singleOrNull() + assertNotNull(refI, "There should be an argument for the call to println") + + val valueI = refI.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(0, 4), valueI) + + val refA = f5.mcalls["f"]?.arguments?.firstOrNull() + assertNotNull(refA, "There should be an argument for the call to f") + + val value = refA.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(5, LatticeInterval.Bound.INFINITE), value) + } + + /* + int i = 0; + for (i = 0; i < 5; i++) { + if(i < 3) { + lessThanThree(i); + } else { + greaterEqualThree(i); + } + println(i); + } + + afterLoop(i); + */ + @Test + fun testFancyLoopInteger() { + val mainClass = tu.records["Foo"] + assertNotNull(mainClass) + val f6 = mainClass.methods["f6"] + assertNotNull(f6) + + val iLessThanThree = f6.calls["lessThanThree"]?.arguments?.singleOrNull() + assertNotNull(iLessThanThree, "There should be an argument for the call to lessThanThree") + + var valueI = iLessThanThree.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(0, 2), valueI) + + val iGreaterEqualThree = f6.calls["greaterEqualThree"]?.arguments?.singleOrNull() + assertNotNull( + iGreaterEqualThree, + "There should be an argument for the call to greaterEqualThree", + ) + + valueI = iGreaterEqualThree.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(3, 4), valueI) + + val refI = f6.calls["println"]?.arguments?.singleOrNull() + assertNotNull(refI, "There should be an argument for the call to println") + + valueI = refI.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(0, 4), valueI) + + val iAfterLoop = f6.calls["afterLoop"]?.arguments?.firstOrNull() + assertNotNull(iAfterLoop, "There should be an argument for the call to afterLoop") + + val value = iAfterLoop.evaluate(IntegerIntervalEvaluator()) + assertEquals(LatticeInterval.Bounded(5, LatticeInterval.Bound.INFINITE), value) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeIntervalTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeIntervalTest.kt new file mode 100644 index 00000000000..45f7860e790 --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/LatticeIntervalTest.kt @@ -0,0 +1,390 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval.BOTTOM +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval.Bound.INFINITE +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval.Bound.NEGATIVE_INFINITE +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval.Bounded +import kotlin.test.* +import kotlin.test.Test +import org.junit.jupiter.api.assertDoesNotThrow + +class LatticeIntervalTest { + @Test + fun testCreate() { + assertDoesNotThrow { BOTTOM } + assertDoesNotThrow { Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) } + assertDoesNotThrow { Bounded(INFINITE, INFINITE) } + assertDoesNotThrow { Bounded(NEGATIVE_INFINITE, INFINITE) } + assertDoesNotThrow { Bounded(0, 0) } + assertDoesNotThrow { Bounded(-5, 5) } + + // Test whether the arguments are switched if necessary + assertEquals(Bounded(NEGATIVE_INFINITE, INFINITE), Bounded(INFINITE, NEGATIVE_INFINITE)) + assertEquals(Bounded(-5, 5), Bounded(5, -5)) + } + + @Test + fun testCompare() { + // comparison including BOTTOM + assertEquals(0, BOTTOM.compareTo(BOTTOM)) + assertEquals(-1, BOTTOM.compareTo(Bounded(-1, 1))) + assertEquals(-1, BOTTOM.compareTo(Bounded(NEGATIVE_INFINITE, INFINITE))) + assertEquals(1, Bounded(-1, 1).compareTo(BOTTOM)) + assertEquals(1, Bounded(NEGATIVE_INFINITE, INFINITE).compareTo(BOTTOM)) + + // comparison with non-overlapping intervals + assertEquals(-1, Bounded(NEGATIVE_INFINITE, -1).compareTo(Bounded(1, INFINITE))) + assertEquals(1, Bounded(1, INFINITE).compareTo(Bounded(NEGATIVE_INFINITE, -1))) + + // comparison with overlapping intervals + assertEquals(0, Bounded(NEGATIVE_INFINITE, 0).compareTo(Bounded(0, INFINITE))) + assertEquals(0, Bounded(-4, 2).compareTo(Bounded(-2, 4))) + assertEquals(0, Bounded(-5, 5).compareTo(Bounded(1, 3))) + assertEquals(0, Bounded(-3, 1).compareTo(Bounded(-5, 5))) + } + + @Test + fun testEquals() { + // comparison with BOTTOM + assertFalse(BOTTOM.equals(null)) + assertFalse(BOTTOM.equals(Bounded(0, 0))) + assertFalse(Bounded(0, 0).equals(BOTTOM)) + assertEquals(BOTTOM, BOTTOM) + + // comparison with different Intervals + assertNotEquals(Bounded(NEGATIVE_INFINITE, 0), Bounded(0, INFINITE)) + assertNotEquals(Bounded(0, 10), Bounded(0, 9)) + assertNotEquals(Bounded(0, 10), Bounded(1, 10)) + assertNotEquals(Bounded(0, 10), Bounded(5, 6)) + assertNotEquals(Bounded(0, 9), Bounded(0, 10)) + assertNotEquals(Bounded(1, 10), Bounded(0, 10)) + assertNotEquals(Bounded(5, 6), Bounded(0, 10)) + + // comparison with same Intervals + assertEquals(Bounded(-5, 5), Bounded(-5, 5)) + assertEquals(Bounded(0, 0), Bounded(0, 0)) + assertEquals(Bounded(NEGATIVE_INFINITE, INFINITE), Bounded(NEGATIVE_INFINITE, INFINITE)) + } + + @Test + fun testAddition() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM + Bounded(5, 5)) + assertEquals(BOTTOM, Bounded(5, 5) + BOTTOM) + assertEquals(BOTTOM, BOTTOM + Bounded(NEGATIVE_INFINITE, INFINITE)) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE) + BOTTOM) + assertEquals(BOTTOM, BOTTOM + BOTTOM) + + // Without BOTTOM + assertEquals(Bounded(INFINITE, INFINITE), Bounded(INFINITE, INFINITE) + Bounded(-5, 5)) + assertEquals( + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) + Bounded(-5, 5), + ) + assertEquals(Bounded(INFINITE, INFINITE), Bounded(-5, 5) + Bounded(INFINITE, INFINITE)) + assertEquals( + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + Bounded(-5, 5) + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + ) + assertEquals(Bounded(-8, 8), Bounded(-5, 5) + Bounded(-3, 3)) + + // Illegal Operations + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) + Bounded(0, INFINITE), + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(INFINITE, INFINITE) + Bounded(NEGATIVE_INFINITE, 0), + ) + } + + @Test + fun testSubtraction() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM - Bounded(5, 5)) + assertEquals(BOTTOM, Bounded(5, 5) - BOTTOM) + assertEquals(BOTTOM, BOTTOM - Bounded(NEGATIVE_INFINITE, INFINITE)) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE) - BOTTOM) + assertEquals(BOTTOM, BOTTOM - BOTTOM) + + // Without BOTTOM + assertEquals(Bounded(INFINITE, INFINITE), Bounded(INFINITE, INFINITE) - Bounded(-5, 5)) + assertEquals( + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) - Bounded(-5, 5), + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + Bounded(-5, 5) - Bounded(INFINITE, INFINITE), + ) + assertEquals( + Bounded(INFINITE, INFINITE), + Bounded(-5, 5) - Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + ) + assertEquals(Bounded(-8, 8), Bounded(-5, 5) - Bounded(-3, 3)) + + // Illegal Operations + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(-10, INFINITE) - Bounded(0, INFINITE), + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(NEGATIVE_INFINITE, 10) - Bounded(NEGATIVE_INFINITE, 0), + ) + } + + @Test + fun testMultiplication() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM * Bounded(5, 5)) + assertEquals(BOTTOM, Bounded(5, 5) * BOTTOM) + assertEquals(BOTTOM, BOTTOM * Bounded(NEGATIVE_INFINITE, INFINITE)) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE) * BOTTOM) + assertEquals(BOTTOM, BOTTOM * BOTTOM) + + // Without BOTTOM + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(INFINITE, INFINITE) * Bounded(-5, 5), + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) * Bounded(-5, 5), + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(-5, 5) * Bounded(INFINITE, INFINITE), + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(-5, 5) * Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + ) + assertEquals(Bounded(15, 15), Bounded(-5, 5) * Bounded(-3, 3)) + + // Illegal Operations + assertEquals( + Bounded(NEGATIVE_INFINITE, 0), + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) * Bounded(0, INFINITE), + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, 0), + Bounded(INFINITE, INFINITE) * Bounded(NEGATIVE_INFINITE, 0), + ) + } + + @Test + fun testDivision() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM / Bounded(5, 5)) + assertEquals(BOTTOM, Bounded(5, 5) / BOTTOM) + assertEquals(BOTTOM, BOTTOM / Bounded(NEGATIVE_INFINITE, INFINITE)) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE) / BOTTOM) + assertEquals(BOTTOM, BOTTOM / BOTTOM) + + // Without BOTTOM + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(INFINITE, INFINITE) / Bounded(-5, 5), + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) / Bounded(-5, 5), + ) + assertEquals(Bounded(0, 0), Bounded(-5, 5) / Bounded(INFINITE, INFINITE)) + assertEquals(Bounded(0, 0), Bounded(-5, 5) / Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE)) + assertEquals(Bounded(5, 5), Bounded(-15, 15) / Bounded(-3, 3)) + + // Illegal Operations + assertEquals( + LatticeInterval.TOP, + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) / Bounded(1, INFINITE), + ) + + assertEquals( + LatticeInterval.TOP, + Bounded(INFINITE, INFINITE) / Bounded(NEGATIVE_INFINITE, 1), + ) + + assertEquals( + LatticeInterval.TOP, + Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) / Bounded(NEGATIVE_INFINITE, 1), + ) + + assertEquals(LatticeInterval.TOP, Bounded(INFINITE, INFINITE) / Bounded(1, INFINITE)) + + assertEquals(LatticeInterval.TOP, Bounded(2, 4) / Bounded(-1, 0)) + } + + @Test + fun testModulo() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM % Bounded(5, 5)) + assertEquals(BOTTOM, Bounded(5, 5) % BOTTOM) + assertEquals(BOTTOM, BOTTOM % Bounded(NEGATIVE_INFINITE, INFINITE)) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE) % BOTTOM) + assertEquals(BOTTOM, BOTTOM % BOTTOM) + + // Without BOTTOM + assertEquals(Bounded(0, 10), Bounded(INFINITE, INFINITE) % Bounded(5, 10)) + assertEquals(Bounded(0, 10), Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE) % Bounded(5, 10)) + assertEquals(Bounded(-5, 5), Bounded(-5, 5) % Bounded(INFINITE, INFINITE)) + assertEquals(Bounded(-1, 1), Bounded(-10, 10) % Bounded(-3, 3)) + + // Illegal Operations + + assertEquals(LatticeInterval.TOP, Bounded(-5, 5) % Bounded(0, 5)) + + assertEquals(LatticeInterval.TOP, Bounded(-5, 5) % Bounded(-5, 0)) + + assertEquals( + LatticeInterval.TOP, + Bounded(-5, 5) % Bounded(NEGATIVE_INFINITE, NEGATIVE_INFINITE), + ) + } + + @Test + fun testJoin() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM.join(Bounded(5, 5))) + assertEquals(BOTTOM, Bounded(5, 5).join(BOTTOM)) + assertEquals(BOTTOM, BOTTOM.join(Bounded(NEGATIVE_INFINITE, INFINITE))) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE).join(BOTTOM)) + assertEquals(BOTTOM, BOTTOM.join(BOTTOM)) + + // Without BOTTOM + assertEquals( + Bounded(NEGATIVE_INFINITE, 10), + Bounded(5, 10).join(Bounded(NEGATIVE_INFINITE, -5)), + ) + assertEquals(Bounded(-10, INFINITE), Bounded(-10, -5).join(Bounded(5, INFINITE))) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(0, 0).join(Bounded(NEGATIVE_INFINITE, INFINITE)), + ) + assertEquals(Bounded(-10, 10), Bounded(9, 10).join(Bounded(-10, -9))) + } + + @Test + fun testMeet() { + // With BOTTOM + assertEquals(Bounded(5, 5), BOTTOM.meet(Bounded(5, 5))) + assertEquals(Bounded(5, 5), Bounded(5, 5).meet(BOTTOM)) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + BOTTOM.meet(Bounded(NEGATIVE_INFINITE, INFINITE)), + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(NEGATIVE_INFINITE, INFINITE).meet(BOTTOM), + ) + assertEquals(BOTTOM, BOTTOM.meet(BOTTOM)) + + // Without BOTTOM + assertEquals(BOTTOM, Bounded(5, 10).meet(Bounded(NEGATIVE_INFINITE, -5))) + assertEquals(BOTTOM, Bounded(-10, -5).meet(Bounded(5, INFINITE))) + assertEquals(BOTTOM, Bounded(9, 10).meet(Bounded(-10, -9))) + assertEquals(Bounded(0, 0), Bounded(0, 0).meet(Bounded(NEGATIVE_INFINITE, INFINITE))) + assertEquals(Bounded(-9, 9), Bounded(-9, 10).meet(Bounded(-10, 9))) + } + + @Test + fun testWiden() { + // With BOTTOM + assertEquals(Bounded(5, 5), BOTTOM.widen(Bounded(5, 5))) + assertEquals(Bounded(5, 5), Bounded(5, 5).widen(BOTTOM)) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + BOTTOM.widen(Bounded(NEGATIVE_INFINITE, INFINITE)), + ) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(NEGATIVE_INFINITE, INFINITE).widen(BOTTOM), + ) + assertEquals(BOTTOM, BOTTOM.widen(BOTTOM)) + + // Without BOTTOM + assertEquals( + Bounded(NEGATIVE_INFINITE, 10), + Bounded(5, 10).widen(Bounded(NEGATIVE_INFINITE, -5)), + ) + assertEquals(Bounded(-10, INFINITE), Bounded(-10, -5).widen(Bounded(5, INFINITE))) + assertEquals( + Bounded(NEGATIVE_INFINITE, INFINITE), + Bounded(0, 0).widen(Bounded(NEGATIVE_INFINITE, INFINITE)), + ) + assertEquals(Bounded(NEGATIVE_INFINITE, 10), Bounded(9, 10).widen(Bounded(-10, -9))) + assertEquals(Bounded(-10, INFINITE), Bounded(-10, -9).widen(Bounded(9, 10))) + } + + @Test + fun testNarrow() { + // With BOTTOM + assertEquals(BOTTOM, BOTTOM.narrow(Bounded(5, 5))) + assertEquals(BOTTOM, Bounded(5, 5).narrow(BOTTOM)) + assertEquals(BOTTOM, BOTTOM.narrow(Bounded(NEGATIVE_INFINITE, INFINITE))) + assertEquals(BOTTOM, Bounded(NEGATIVE_INFINITE, INFINITE).narrow(BOTTOM)) + assertEquals(BOTTOM, BOTTOM.narrow(BOTTOM)) + + // Without BOTTOM + assertEquals(Bounded(5, 10), Bounded(5, 10).narrow(Bounded(NEGATIVE_INFINITE, -5))) + assertEquals(Bounded(-10, -5), Bounded(-10, -5).narrow(Bounded(5, INFINITE))) + assertEquals(Bounded(0, 0), Bounded(0, 0).narrow(Bounded(NEGATIVE_INFINITE, INFINITE))) + assertEquals(Bounded(-5, 5), Bounded(NEGATIVE_INFINITE, -5).narrow(Bounded(5, 10))) + assertEquals(Bounded(-5, 5), Bounded(5, INFINITE).narrow(Bounded(-10, -5))) + assertEquals(Bounded(0, 0), Bounded(NEGATIVE_INFINITE, INFINITE).narrow(Bounded(0, 0))) + } + + @Test + fun testToString() { + assertEquals("[-5, 5]", Bounded(-5, 5).toString()) + } + + @Test + fun testWrapper() { + val bottom = BOTTOM + val zero = Bounded(0, 0) + val outer = Bounded(-5, 5) + val infinity = Bounded(NEGATIVE_INFINITE, INFINITE) + + // compare to + assertEquals(-1, bottom.compareTo(zero)) + assertEquals(0, zero.compareTo(zero)) + assertEquals(1, zero.compareTo(bottom)) + + // widen + assertEquals(outer, outer.widen(zero)) + assertEquals(infinity, zero.widen(outer)) + + // narrow + assertEquals(outer, infinity.narrow(outer)) + assertEquals(outer, outer.narrow(zero)) + assertEquals(outer, outer.narrow(infinity)) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValueTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValueTest.kt new file mode 100644 index 00000000000..1a4aa2a5011 --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/ArrayValueTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.* +import de.fraunhofer.aisec.cpg.analysis.abstracteval.IntervalLattice +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.frontends.TestLanguage +import de.fraunhofer.aisec.cpg.graph.array +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.NewArrayExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference +import de.fraunhofer.aisec.cpg.graph.types.IntegerType +import kotlin.test.Test +import kotlin.test.assertEquals + +class ArrayValueTest { + private val lattice = + TupleState(DeclarationState(IntervalLattice()), NewIntervalState(IntervalLattice())) + + @Test + fun applyDeclarationTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + + val correctDeclaration = + VariableDeclaration().apply { + this.name = name + this.type = IntegerType(language = TestLanguage()).array() + this.initializer = + NewArrayExpression().apply { + this.dimensions += Literal().apply { this.value = 5 } + } + } + + assertEquals( + LatticeInterval.Bounded(5, 5), + ArrayValue() + .applyEffect(lattice = lattice, state = startState, node = correctDeclaration), + ) + } + + @Test + fun applyReferenceTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + + val decl = + VariableDeclaration().apply { + this.name = name + this.type = IntegerType(language = TestLanguage()).array() + this.initializer = + NewArrayExpression().apply { + this.dimensions += Literal().apply { this.value = 5 } + } + } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(5, 5)) + val reference = + Reference().apply { + this.name = name + this.refersTo = decl + } + + assertEquals( + LatticeInterval.Bounded(5, 5), + ArrayValue().applyEffect(lattice = lattice, state = startState, node = reference), + ) + } + + @Test + fun applyDeclarationWithoutInitializerTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val noInitializerDeclaration = + VariableDeclaration().apply { + this.name = name + this.type = IntegerType(language = TestLanguage()).array() + } + + assertEquals( + LatticeInterval.TOP, + ArrayValue() + .applyEffect(lattice = lattice, state = startState, node = noInitializerDeclaration), + ) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValueTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValueTest.kt new file mode 100644 index 00000000000..0033c18c89f --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/IntegerValueTest.kt @@ -0,0 +1,534 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.* +import de.fraunhofer.aisec.cpg.analysis.abstracteval.IntervalLattice +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval.Bound.* +import de.fraunhofer.aisec.cpg.graph.Name +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.AssignExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference +import de.fraunhofer.aisec.cpg.graph.statements.expressions.UnaryOperator +import de.fraunhofer.aisec.cpg.passes.objectIdentifier +import kotlin.test.Test +import kotlin.test.assertEquals + +class IntegerValueTest { + private val name = Name("testVariable") + private val current = LatticeInterval.Bounded(1, 1) + val lattice = + TupleState(DeclarationState(IntervalLattice()), NewIntervalState(IntervalLattice())) + + @Test + fun applyDeclarationTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val declaration = + VariableDeclaration().apply { + name = this@IntegerValueTest.name + initializer = + Literal().apply { + value = 5 + this.value?.let { value -> + startState.first[this.objectIdentifier()] = + IntervalLattice.Element(LatticeInterval.Bounded(value, value)) + } + } + } + assertEquals( + LatticeInterval.Bounded(5, 5), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = declaration), + ) + } + + @Test + fun applyUninitializedDeclarationTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val declaration = VariableDeclaration().apply { name = this@IntegerValueTest.name } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = declaration), + ) + } + + @Test + fun applyPrefixIncrement() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val reference = + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + val unaryOperator = + UnaryOperator().apply { + isPrefix = true + operatorCode = "++" + input = reference + } + assertEquals( + LatticeInterval.Bounded(2, 2), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = unaryOperator), + ) + } + + @Test + fun applyPostfixIncrement() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val reference = + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + val unaryOperator = + UnaryOperator().apply { + isPrefix = false + operatorCode = "++" + input = reference + } + assertEquals( + LatticeInterval.Bounded(2, 2), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = unaryOperator), + ) + } + + @Test + fun applyPrefixDecrement() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val reference = + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + val unaryOperator = + UnaryOperator().apply { + isPrefix = true + operatorCode = "--" + input = reference + } + assertEquals( + LatticeInterval.Bounded(0, 0), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = unaryOperator), + ) + } + + @Test + fun applyPostfixIncrementation() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val reference = + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + val unaryOperator = + UnaryOperator().apply { + isPrefix = false + operatorCode = "--" + input = reference + } + assertEquals( + LatticeInterval.Bounded(0, 0), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = unaryOperator), + ) + } + + @Test + fun applyUnaryStar() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val reference = + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + val unaryOperator = + UnaryOperator().apply { + operatorCode = "*" + input = reference + } + assertEquals( + LatticeInterval.Bounded(1, 1), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = unaryOperator), + ) + } + + @Test + fun applyAssignExpression() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + rhs += + Literal().apply { + value = 3 + this.value?.let { value -> + startState.first[this.objectIdentifier()] = + IntervalLattice.Element(LatticeInterval.Bounded(value, value)) + } + } + } + assertEquals( + LatticeInterval.Bounded(3, 3), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } + + @Test + fun testAssignUnresolved() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } + + @Test + fun testAssignPlusLiteral() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "+=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + rhs += + Literal().apply { + value = 3 + this.value?.let { value -> + startState.first[this.objectIdentifier()] = + IntervalLattice.Element(LatticeInterval.Bounded(value, value)) + } + } + } + assertEquals( + LatticeInterval.Bounded(4, 4), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } + + @Test + fun testAssignPlusUnresolved() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "+=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } + + @Test + fun testAssignMinusLiteral() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "-=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + rhs += + Literal().apply { + value = 3 + this.value?.let { value -> + startState.first[this.objectIdentifier()] = + IntervalLattice.Element(LatticeInterval.Bounded(value, value)) + } + } + } + assertEquals( + LatticeInterval.Bounded(-2, -2), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } + + @Test + fun testAssignMinusUnresolved() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "-=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } + + @Test + fun testAssignTimesLiteral() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "*=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + rhs += + Literal().apply { + value = 3 + this.value?.let { value -> + startState.first[this.objectIdentifier()] = + IntervalLattice.Element(LatticeInterval.Bounded(value, value)) + } + } + } + assertEquals( + LatticeInterval.Bounded(3, 3), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } + + @Test + fun testAssignTimesUnresolved() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "*=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } + + @Test + fun testAssignDivLiteral() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "/=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + rhs += + Literal().apply { + value = 3 + this.value?.let { value -> + startState.first[this.objectIdentifier()] = + IntervalLattice.Element(LatticeInterval.Bounded(value, value)) + } + } + } + assertEquals( + LatticeInterval.Bounded(0, 0), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } + + @Test + fun testAssignDivUnresolved() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "/=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } + + @Test + fun testAssignModLiteral() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "%=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + rhs += + Literal().apply { + value = 3 + this.value?.let { value -> + startState.first[this.objectIdentifier()] = + IntervalLattice.Element(LatticeInterval.Bounded(value, value)) + } + } + } + assertEquals( + LatticeInterval.Bounded(1, 1), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } + + @Test + fun testAssignModUnresolved() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val assignment = + AssignExpression().apply { + operatorCode = "%=" + lhs += + Reference().apply { + name = this@IntegerValueTest.name + refersTo = VariableDeclaration().apply { name = this@IntegerValueTest.name } + startState.first[objectIdentifier()] = IntervalLattice.Element(current) + } + } + assertEquals( + LatticeInterval.Bounded(NEGATIVE_INFINITE, INFINITE), + IntegerValue().applyEffect(lattice = lattice, state = startState, node = assignment), + ) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListValueTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListValueTest.kt new file mode 100644 index 00000000000..1bfd37d517f --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableListValueTest.kt @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.* +import de.fraunhofer.aisec.cpg.analysis.abstracteval.IntervalLattice +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.frontends.TestLanguage +import de.fraunhofer.aisec.cpg.graph.Name +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference +import de.fraunhofer.aisec.cpg.graph.types.IntegerType +import kotlin.test.Test +import kotlin.test.assertEquals + +class MutableListValueTest { + private val lattice = + TupleState(DeclarationState(IntervalLattice()), NewIntervalState(IntervalLattice())) + + @Test + fun applyDeclarationTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val correctDeclaration = + VariableDeclaration().apply { + this.name = name + this.initializer = + MemberCallExpression().apply { + this.arguments += Literal().apply { this.value = 5 } + this.arguments += Literal().apply { this.value = 5 } + } + } + + assertEquals( + LatticeInterval.Bounded(2, 2), + MutableListSize() + .applyEffect(lattice = lattice, state = startState, node = correctDeclaration), + ) + } + + @Test + fun applyDeclarationNoInitializerTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val noInitializerDeclaration = VariableDeclaration().apply { this.name = name } + + assertEquals( + LatticeInterval.BOTTOM, + MutableListSize() + .applyEffect(lattice = lattice, state = startState, node = noInitializerDeclaration), + ) + } + + @Test + fun applyAddTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val decl = VariableDeclaration().apply { this.name = name } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(1, 1)) + + val add = + MemberCallExpression().apply { + this.name = Name("add") + callee = + MemberExpression().apply { + this.name = Name("add") + base = + Reference().apply { + this.name = name + this.refersTo = decl + } + } + this.arguments += Literal().apply { this.value = 5 } + } + + assertEquals( + LatticeInterval.Bounded(2, 2), + MutableListSize().applyEffect(lattice = lattice, state = startState, node = add), + ) + } + + @Test + fun applyAddAllTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val decl = VariableDeclaration().apply { this.name = name } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(1, 1)) + + val addAll = + MemberCallExpression().apply { + this.name = Name("addAll") + callee = + MemberExpression().apply { + this.name = Name("addAll") + base = + Reference().apply { + this.name = name + this.refersTo = decl + } + } + this.arguments += Literal().apply { this.value = 5 } + this.arguments += Literal().apply { this.value = 10 } + } + + assertEquals( + LatticeInterval.Bounded(3, 3), + MutableListSize().applyEffect(lattice = lattice, state = startState, node = addAll), + ) + } + + @Test + fun applyClearTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val decl = VariableDeclaration().apply { this.name = name } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(3, 3)) + + val clear = + MemberCallExpression().apply { + this.name = Name("clear") + callee = + MemberExpression().apply { + this.name = Name("clear") + base = + Reference().apply { + this.name = name + this.refersTo = decl + } + } + } + + assertEquals( + LatticeInterval.Bounded(0, 0), + MutableListSize().applyEffect(lattice = lattice, state = startState, node = clear), + ) + } + + @Test + fun applyRemoveIndexedTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val decl = VariableDeclaration().apply { this.name = name } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(3, 3)) + + val removeInt = + MemberCallExpression().apply { + this.name = Name("remove") + callee = + MemberExpression().apply { + this.name = Name("remove") + base = + Reference().apply { + this.name = name + this.refersTo = decl + } + } + this.arguments += + Literal().apply { + this.value = 5 + this.type = IntegerType(language = TestLanguage()) + } + } + + assertEquals( + LatticeInterval.Bounded(2, 2), + MutableListSize().applyEffect(lattice = lattice, state = startState, node = removeInt), + ) + } + + @Test + fun applyRemoveObjectTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val decl = VariableDeclaration().apply { this.name = name } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(3, 3)) + + val removeObject = + MemberCallExpression().apply { + this.name = Name("remove") + callee = + MemberExpression().apply { + this.name = Name("remove") + base = + Reference().apply { + this.name = name + this.refersTo = decl + } + } + this.arguments += Literal().apply { this.value = 5 } + } + + assertEquals( + LatticeInterval.Bounded(2, 3), + MutableListSize() + .applyEffect(lattice = lattice, state = startState, node = removeObject), + ) + } + + @Test + fun applyRemoveAllTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val decl = VariableDeclaration().apply { this.name = name } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(3, 3)) + + val removeAll = + MemberCallExpression().apply { + this.name = Name("removeAll") + callee = + MemberExpression().apply { + this.name = Name("removeAll") + base = + Reference().apply { + this.name = name + this.refersTo = decl + } + } + this.arguments += Literal().apply { this.value = 5 } + this.arguments += Literal().apply { this.value = 10 } + } + assertEquals( + LatticeInterval.Bounded(0, 3), + MutableListSize().applyEffect(lattice = lattice, state = startState, node = removeAll), + ) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableSetValueTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableSetValueTest.kt new file mode 100644 index 00000000000..2e758111e98 --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/value/MutableSetValueTest.kt @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2024, 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.cpg.analysis.abstracteval.value + +import de.fraunhofer.aisec.cpg.analysis.abstracteval.* +import de.fraunhofer.aisec.cpg.analysis.abstracteval.IntervalLattice +import de.fraunhofer.aisec.cpg.analysis.abstracteval.LatticeInterval +import de.fraunhofer.aisec.cpg.frontends.TestLanguage +import de.fraunhofer.aisec.cpg.graph.Name +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference +import de.fraunhofer.aisec.cpg.graph.types.ListType +import de.fraunhofer.aisec.cpg.graph.types.ObjectType +import de.fraunhofer.aisec.cpg.graph.types.SetType +import kotlin.test.Test +import kotlin.test.assertEquals + +class MutableSetValueTest { + private val lattice = + TupleState(DeclarationState(IntervalLattice()), NewIntervalState(IntervalLattice())) + + @Test + fun applyDeclarationTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val correctDeclaration = + VariableDeclaration().apply { + this.name = name + this.initializer = + MemberCallExpression().apply { + this.arguments += Literal().apply { this.value = 5 } + this.arguments += Literal().apply { this.value = 5 } + } + } + + assertEquals( + LatticeInterval.Bounded(1, 2), + MutableSetSize() + .applyEffect(lattice = lattice, state = startState, node = correctDeclaration), + ) + } + + @Test + fun applyDeclarationFromListTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val existingDecl = + VariableDeclaration().apply { + this.name = Name("existingDecl") + this.type = ListType("list", elementType = ObjectType(), language = TestLanguage()) + } + lattice.pushToDeclarationState(startState, existingDecl, LatticeInterval.Bounded(2, 3)) + + val correctDeclaration = + VariableDeclaration().apply { + this.name = name + this.initializer = + MemberCallExpression().apply { + this.name = Name("toSet") + this.callee = + MemberExpression().apply { + this.name = Name("toSet") + this.base = + Reference().apply { + this.type = + ListType( + "list", + elementType = ObjectType(), + language = TestLanguage(), + ) + this.name = Name("existingDecl") + this.refersTo = existingDecl + } + } + } + } + + assertEquals( + LatticeInterval.Bounded(1, 3), + MutableSetSize() + .applyEffect(lattice = lattice, state = startState, node = correctDeclaration), + ) + } + + @Test + fun applyDeclarationFromSetTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val existingDecl = + VariableDeclaration().apply { + this.name = Name("existingDecl") + this.type = SetType("set", elementType = ObjectType(), language = TestLanguage()) + } + lattice.pushToDeclarationState(startState, existingDecl, LatticeInterval.Bounded(2, 3)) + + val correctDeclaration = + VariableDeclaration().apply { + this.name = name + this.initializer = + MemberCallExpression().apply { + this.name = Name("toSet") + this.callee = + MemberExpression().apply { + this.name = Name("toSet") + this.base = + Reference().apply { + this.type = + SetType( + "set", + elementType = ObjectType(), + language = TestLanguage(), + ) + this.name = Name("existingDecl") + this.refersTo = existingDecl + } + } + } + } + + assertEquals( + LatticeInterval.Bounded(2, 3), + MutableSetSize() + .applyEffect(lattice = lattice, state = startState, node = correctDeclaration), + ) + } + + @Test + fun applyDeclarationNoInitializerTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val noInitializerDeclaration = VariableDeclaration().apply { this.name = name } + + assertEquals( + LatticeInterval.BOTTOM, + MutableSetSize() + .applyEffect(lattice = lattice, state = startState, node = noInitializerDeclaration), + ) + } + + @Test + fun applyAddTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val decl = VariableDeclaration().apply { this.name = name } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(1, 1)) + + val add = + MemberCallExpression().apply { + this.name = Name("add") + callee = + MemberExpression().apply { + this.name = Name("add") + base = + Reference().apply { + this.name = name + this.refersTo = decl + } + } + this.arguments += Literal().apply { this.value = 5 } + } + + assertEquals( + LatticeInterval.Bounded(1, 2), + MutableSetSize().applyEffect(lattice = lattice, state = startState, node = add), + ) + } + + @Test + fun applyAddAllTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val decl = VariableDeclaration().apply { this.name = name } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(1, 1)) + + val addAll = + MemberCallExpression().apply { + this.name = Name("addAll") + callee = + MemberExpression().apply { + this.name = Name("addAll") + base = + Reference().apply { + this.name = name + this.refersTo = decl + } + } + this.arguments += Literal().apply { this.value = 5 } + this.arguments += Literal().apply { this.value = 10 } + } + + assertEquals( + LatticeInterval.Bounded(1, 3), + MutableSetSize().applyEffect(lattice = lattice, state = startState, node = addAll), + ) + } + + @Test + fun applyClearTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val decl = VariableDeclaration().apply { this.name = name } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(3, 3)) + + val clear = + MemberCallExpression().apply { + this.name = Name("clear") + callee = + MemberExpression().apply { + this.name = Name("clear") + base = + Reference().apply { + this.name = name + this.refersTo = decl + } + } + } + + assertEquals( + LatticeInterval.Bounded(0, 0), + MutableSetSize().applyEffect(lattice = lattice, state = startState, node = clear), + ) + } + + @Test + fun applyRemoveObjectTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val decl = VariableDeclaration().apply { this.name = name } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(3, 3)) + + val removeObject = + MemberCallExpression().apply { + this.name = Name("remove") + callee = + MemberExpression().apply { + this.name = Name("remove") + base = + Reference().apply { + this.name = name + this.refersTo = decl + } + } + this.arguments += Literal().apply { this.value = 5 } + } + + assertEquals( + LatticeInterval.Bounded(2, 3), + MutableSetSize().applyEffect(lattice = lattice, state = startState, node = removeObject), + ) + } + + @Test + fun applyRemoveAllTest() { + val startState = + TupleStateElement( + DeclarationState.DeclarationStateElement(), + NewIntervalStateElement(), + ) + val decl = VariableDeclaration().apply { this.name = name } + lattice.pushToDeclarationState(startState, decl, LatticeInterval.Bounded(3, 3)) + + val removeAll = + MemberCallExpression().apply { + this.name = Name("removeAll") + callee = + MemberExpression().apply { + this.name = Name("removeAll") + base = + Reference().apply { + this.name = name + this.refersTo = decl + } + } + this.arguments += Literal().apply { this.value = 5 } + this.arguments += Literal().apply { this.value = 10 } + } + assertEquals( + LatticeInterval.Bounded(0, 3), + MutableSetSize().applyEffect(lattice = lattice, state = startState, node = removeAll), + ) + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/query/QueryTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/query/QueryTest.kt index de32fbb6f04..3922e29e0d6 100644 --- a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/query/QueryTest.kt +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/query/QueryTest.kt @@ -515,7 +515,7 @@ class QueryTest { @Test fun testDivisionBy0() { - val result = Query.getVulnerable() + val result = Query.getDivBy0() val queryTreeResult = result.all( diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/AbstractEvaluationTests.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/AbstractEvaluationTests.kt new file mode 100644 index 00000000000..9d66b089d4b --- /dev/null +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/AbstractEvaluationTests.kt @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2024, 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.cpg.testcases + +import de.fraunhofer.aisec.cpg.TranslationConfiguration +import de.fraunhofer.aisec.cpg.frontends.TestLanguage +import de.fraunhofer.aisec.cpg.frontends.testFrontend +import de.fraunhofer.aisec.cpg.graph.builder.* +import de.fraunhofer.aisec.cpg.graph.newBinaryOperator +import de.fraunhofer.aisec.cpg.graph.newLiteral +import de.fraunhofer.aisec.cpg.graph.newReference +import de.fraunhofer.aisec.cpg.passes.UnreachableEOGPass + +abstract class AbstractEvaluationTests { + companion object { + /* + public class IntegerTest { + public void f1() { + Bar b = new Bar(); + int a = 5; + + a = 0; + a -= 2; + a += 3; + + b.f(a); + } + + public void f2() { + Bar f = new Bar(); + int a = 5; + + a = 3; + a++; + ++a; + a -= 2; + a += 3; + a--; + --a; + a *= 4; + a /= 2; + a %= 3; + + b.f(a); + } + + public void f3() { + Bar b = new Bar(); + int a = 5; + + if (new Random().nextBoolean()) { + a -= 1; + } + + b.f(a); + } + + public void f4() { + Bar b = new Bar(); + int a = 5; + + if (new Random().nextBoolean()) { + a -= 1; + } else { + a = 3; + } + + b.f(a); + } + + public void f5() { + Bar b = new Bar(); + int a = 5; + + for (int i = 0; i < 5; i++) { + a += 1; + } + + b.f(a); + } + } + + class Bar { + public void f(int a) {} + } + */ + fun getIntegerExample( + config: TranslationConfiguration = + TranslationConfiguration.builder() + .defaultPasses() + .registerLanguage() + .registerPass() + .build() + ) = + testFrontend(config).build { + translationResult { + translationUnit("integer.java") { + record("Foo") { + method("f1") { + body { + declare { variable("b", t("Bar")) } + declare { variable("a", t("int")) { literal(5, t("int")) } } + + ref("a") assign literal(0, t("int")) + ref("a") assignMinus literal(2, t("int")) + ref("a") assignPlus literal(3, t("int")) + + memberCall("f", ref("Bar")) { ref("a") } + } + } + method("f2") { + body { + declare { variable("b", t("Bar")) } + declare { variable("a", t("int")) { literal(5, t("int")) } } + + ref("a") assign literal(3, t("int")) + + ref("a").inc() + ref("a").incPrefix() + + memberCall("c", ref("Bar")) { ref("a") } + + ref("a") assignMinus literal(2, t("int")) + ref("a") assignPlus literal(3, t("int")) + + ref("a").dec() + ref("a").decPrefix() + + ref("a") assignMult literal(4, t("int")) + ref("a") assignDiv literal(2, t("int")) + ref("a") assignMod literal(3, t("int")) + + memberCall("f", ref("Bar")) { ref("a") } + } + } + method("f3") { + body { + declare { variable("b", t("Bar")) } + declare { variable("a", t("int")) { literal(5, t("int")) } } + + ifStmt { + condition { memberCall("nextBoolean", ref("Random")) } + thenStmt { ref("a") assignMinus literal(1, t("int")) } + } + + memberCall("f", ref("Bar")) { ref("a") } + } + } + method("f4") { + body { + declare { variable("b", t("Bar")) } + declare { variable("a", t("int")) { literal(5, t("int")) } } + + ifStmt { + condition { memberCall("nextBoolean", ref("Random")) } + thenStmt { ref("a") assignMinus literal(1, t("int")) } + elseStmt { ref("a") assign literal(3, t("int")) } + } + + memberCall("f", ref("Bar")) { ref("a") } + } + } + method("f5") { + body { + declare { variable("b", t("Bar")) } + declare { variable("a", t("int")) { literal(5, t("int")) } } + + forStmt { + forInitializer { + declare { + variable("i", t("int")) { literal(0, t("int")) } + } + } + forCondition { ref("i") lt literal(5, t("int")) } + forIteration { ref("i").inc() } + + loopBody { + ref("a") assignPlus literal(1, t("int")) + call("println") { ref("i") } + } + } + + memberCall("f", ref("Bar")) { ref("a") } + } + } + method("f6") { + body { + declare { variable("i", t("int")) { literal(0, t("int")) } } + + forStmt { + forInitializerExpr { ref("i") assign literal(0, t("int")) } + forCondition { ref("i") lt literal(5, t("int")) } + forIteration { ref("i").inc() } + + loopBody { + ifStmt { + condition { + val tmp = + this@build.newBinaryOperator("<").apply { + lhs = newReference("i") + rhs = newLiteral(3, t("int")) + } + condition = tmp + tmp + // ref("i") lt literal(3, t("int")) + } + thenStmt { call("lessThanThree") { ref("i") } } + elseStmt { call("greaterEqualThree") { ref("i") } } + } + call("println") { ref("i") } + } + } + + call("afterLoop") { ref("i") } + } + } + } + record("Bar") { + method("main") { + param("a", t("int")) + body {} + } + } + } + } + } + } +} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Query.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Query.kt index da82004f30a..554b747d5e7 100644 --- a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Query.kt +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Query.kt @@ -471,6 +471,12 @@ class Query { declare { variable("a", t("int")) { literal(0, t("int")) } } forStmt { + forInitializer { + declareVar("i", t("int")) { literal(0, t("int")) } + } + forCondition { ref("i") lt literal(5, t("int")) } + forIteration { ref("i").incNoContext() } + loopBody { ref("a") assign { @@ -481,11 +487,6 @@ class Query { } } } - forInitializer { - declareVar("i", t("int")) { literal(0, t("int")) } - } - forCondition { ref("i") lt literal(5, t("int")) } - forIteration { ref("i").incNoContext() } } returnStmt { ref("a") } @@ -568,6 +569,41 @@ class Query { } } + fun getDivBy0( + config: TranslationConfiguration = + TranslationConfiguration.builder() + .defaultPasses() + .registerLanguage() + .build() + ) = + testFrontend(config).build { + translationResult { + translationUnit("assign.cpp") { + function("main", t("int")) { + body { + declare { + variable("array", t("char").array()) { + literal("hello", t("char").array()) + } + } + declare { variable("a", t("short")) { literal(2, t("int")) } } + + ifStmt { + condition { ref("array") eq literal("hello", t("string")) } + thenStmt { ref("a") assign literal(0, t("int")) } + } + + declare { + variable("x", t("double")) { literal(5, t("int")) / ref("a") } + } + + returnStmt { literal(0, t("int")) } + } + } + } + } + } + fun getVulnerable( config: TranslationConfiguration = TranslationConfiguration.builder() @@ -601,7 +637,7 @@ class Query { ifStmt { condition { ref("array") eq literal("hello", t("string")) } - thenStmt { ref("a") assign literal(0, t("int")) } + thenStmt { ref("a") assign literal(1, t("int")) } } declare { diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt index 2ac493c3418..149e9d74c0d 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt @@ -304,7 +304,7 @@ fun LanguageFrontend<*, *>.returnStmt(init: ReturnStatement.() -> Unit): ReturnS val node = newReturnStatement() init(node) - (holder) += node + holder += node return node } @@ -375,7 +375,7 @@ fun LanguageFrontend<*, *>.declare(init: DeclarationStatement.() -> Unit): Decla val node = newDeclarationStatement() init(node) - (holder) += node + holder += node return node } @@ -599,7 +599,7 @@ fun LanguageFrontend<*, *>.ifStmt(init: IfStatement.() -> Unit): IfStatement { val node = newIfStatement() init(node) - (holder) += node + holder += node return node } @@ -615,7 +615,7 @@ fun LanguageFrontend<*, *>.forEachStmt(init: ForEachStatement.() -> Unit): ForEa init(node) - (holder) += node + holder += node return node } @@ -632,7 +632,7 @@ fun LanguageFrontend<*, *>.forStmt(init: ForStatement.() -> Unit): ForStatement init(node) - (holder) += node + holder += node return node } @@ -651,6 +651,18 @@ fun LanguageFrontend<*, *>.forCondition(init: ForStatement.() -> Expression): Ex return node } +/** + * Configures the [ForStatement.condition] in the Fluent Node DSL of the nearest enclosing + * [ForStatement]. The [init] block can be used to create further sub-nodes as well as configuring + * the created node itself. + */ +context(stmt: ForStatement) +fun LanguageFrontend<*, *>.forInitializerExpr(init: ForStatement.() -> Statement): Statement { + val node = init(stmt) + stmt.initializerStatement = node + return node +} + /** * Configures the [ForStatement.condition] in the Fluent Node DSL of the nearest enclosing * [ForStatement]. The [init] block can be used to create further sub-nodes as well as configuring @@ -678,7 +690,7 @@ fun LanguageFrontend<*, *>.forInitializer( */ context(stmt: ForStatement) fun LanguageFrontend<*, *>.forIteration(init: ForStatement.() -> Statement): Statement { - var node = init(stmt) + val node = init(stmt) stmt.iterationStatement = node return node @@ -699,7 +711,7 @@ fun LanguageFrontend<*, *>.switchStmt( node.selector = selector scopeIfNecessary(needsScope, node, init) - (holder) += node + holder += node return node } @@ -717,7 +729,7 @@ fun LanguageFrontend<*, *>.whileStmt( val node = newWhileStatement() scopeIfNecessary(needsScope, node, init) - (holder) += node + holder += node return node } @@ -735,7 +747,7 @@ fun LanguageFrontend<*, *>.doStmt( val node = newDoStatement() scopeIfNecessary(needsScope, node, init) - (holder) += node + holder += node return node } @@ -1034,6 +1046,7 @@ fun LanguageFrontend<*, *>.ref( ): Reference { val node = newReference(name) node.type = type + node.code = name.toString() if (init != null) { init(node) @@ -1083,7 +1096,7 @@ fun LanguageFrontend<*, *>.member( if (parsedName.parent != null) { unknownType() } else { - var scope = ((holder) as? ScopeProvider)?.scope + var scope = (holder as? ScopeProvider)?.scope while (scope != null && scope !is RecordScope) { scope = scope.parent } @@ -1108,17 +1121,17 @@ fun LanguageFrontend<*, *>.member( */ context(frontend: LanguageFrontend<*, *>, holder: ArgumentHolder) operator fun Expression.times(rhs: Expression): BinaryOperator { - val node = (frontend).newBinaryOperator("*") + val node = frontend.newBinaryOperator("*") node.lhs = this node.rhs = rhs - (holder) += node + holder += node // We need to do a little trick here. Because of the evaluation order, lhs and rhs might also // been added to the argument holders arguments (and we do not want that). However, we cannot // prevent it, so we need to remove them again - (holder) -= node.lhs - (holder) -= node.rhs + holder -= node.lhs + holder -= node.rhs return node } @@ -1129,15 +1142,15 @@ operator fun Expression.times(rhs: Expression): BinaryOperator { */ context(frontend: LanguageFrontend<*, *>, holder: ArgumentHolder) operator fun Expression.unaryMinus(): UnaryOperator { - val node = (frontend).newUnaryOperator("-", false, false) + val node = frontend.newUnaryOperator("-", false, false) node.input = this - (holder) += node + holder += node // We need to do a little trick here. Because of the evaluation order, lhs and rhs might also // been added to the argument holders arguments (and we do not want that). However, we cannot // prevent it, so we need to remove them again - (holder) -= node.input + holder -= node.input return node } @@ -1148,17 +1161,17 @@ operator fun Expression.unaryMinus(): UnaryOperator { */ context(frontend: LanguageFrontend<*, *>, holder: ArgumentHolder) operator fun Expression.div(rhs: Expression): BinaryOperator { - val node = (frontend).newBinaryOperator("/") + val node = frontend.newBinaryOperator("/") node.lhs = this node.rhs = rhs - (holder) += node + holder += node // We need to do a little trick here. Because of the evaluation order, lhs and rhs might also // been added to the argument holders arguments (and we do not want that). However, we cannot // prevent it, so we need to remove them again - (holder) -= node.lhs - (holder) -= node.rhs + holder -= node.lhs + holder -= node.rhs return node } @@ -1169,17 +1182,17 @@ operator fun Expression.div(rhs: Expression): BinaryOperator { */ context(frontend: LanguageFrontend<*, *>, holder: ArgumentHolder) operator fun Expression.plus(rhs: Expression): BinaryOperator { - val node = (frontend).newBinaryOperator("+") + val node = frontend.newBinaryOperator("+") node.lhs = this node.rhs = rhs - (holder) += node + holder += node // We need to do a little trick here. Because of the evaluation order, lhs and rhs might also // been added to the argument holders arguments (and we do not want that). However, we cannot // prevent it, so we need to remove them again - (holder) -= node.lhs - (holder) -= node.rhs + holder -= node.lhs + holder -= node.rhs return node } @@ -1190,9 +1203,9 @@ operator fun Expression.plus(rhs: Expression): BinaryOperator { */ context(frontend: LanguageFrontend<*, *>, holder: StatementHolder) operator fun Expression.plusAssign(rhs: Expression) { - val node = (frontend).newAssignExpression("+=", listOf(this), listOf(rhs)) + val node = frontend.newAssignExpression("+=", listOf(this), listOf(rhs)) - (holder) += node + holder += node } /** @@ -1201,17 +1214,17 @@ operator fun Expression.plusAssign(rhs: Expression) { */ context(frontend: LanguageFrontend<*, *>, holder: ArgumentHolder) operator fun Expression.rem(rhs: Expression): BinaryOperator { - val node = (frontend).newBinaryOperator("%") + val node = frontend.newBinaryOperator("%") node.lhs = this node.rhs = rhs - (holder) += node + holder += node // We need to do a little trick here. Because of the evaluation order, lhs and rhs might also // been added to the argument holders arguments (and we do not want that). However, we cannot // prevent it, so we need to remove them again - (holder) -= node.lhs - (holder) -= node.rhs + holder -= node.lhs + holder -= node.rhs return node } @@ -1222,11 +1235,11 @@ operator fun Expression.rem(rhs: Expression): BinaryOperator { */ context(frontend: LanguageFrontend<*, *>, holder: ArgumentHolder) operator fun Expression.minus(rhs: Expression): BinaryOperator { - val node = (frontend).newBinaryOperator("-") + val node = frontend.newBinaryOperator("-") node.lhs = this node.rhs = rhs - (holder) += node + holder += node return node } @@ -1237,7 +1250,7 @@ operator fun Expression.minus(rhs: Expression): BinaryOperator { */ context(frontend: LanguageFrontend<*, *>, holder: ArgumentHolder) fun reference(input: Expression): UnaryOperator { - val node = (frontend).newUnaryOperator("&", false, false) + val node = frontend.newUnaryOperator("&", false, false) node.input = input holder += node @@ -1246,12 +1259,12 @@ fun reference(input: Expression): UnaryOperator { } /** - * Creates a new [UnaryOperator] with a `--` [UnaryOperator.operatorCode] in the Fluent Node DSL and - * adds it to the nearest enclosing [StatementHolder]. + * Creates a new [UnaryOperator] with a `--` postfix [UnaryOperator.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. */ context(frontend: LanguageFrontend<*, *>, holder: Holder) operator fun Expression.dec(): UnaryOperator { - val node = (frontend).newUnaryOperator("--", true, false) + val node = frontend.newUnaryOperator("--", true, false) node.input = this if (holder is StatementHolder) { @@ -1262,12 +1275,44 @@ operator fun Expression.dec(): UnaryOperator { } /** - * Creates a new [UnaryOperator] with a `++` [UnaryOperator.operatorCode] in the Fluent Node DSL and - * invokes [ArgumentHolder.addArgument] of the nearest enclosing [ArgumentHolder]. + * Creates a new [UnaryOperator] with a `++` postfix [UnaryOperator.operatorCode] in the Fluent Node + * DSL and invokes [ArgumentHolder.addArgument] of the nearest enclosing [ArgumentHolder]. */ context(frontend: LanguageFrontend<*, *>, holder: Holder) operator fun Expression.inc(): UnaryOperator { - val node = (frontend).newUnaryOperator("++", true, false) + val node = frontend.newUnaryOperator("++", true, false) + node.input = this + + if (holder is StatementHolder) { + holder += node + } + + return node +} + +/** + * Creates a new [UnaryOperator] with a `--` prefix [UnaryOperator.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. + */ +context(frontend: LanguageFrontend<*, *>, holder: Holder) +fun Expression.decPrefix(): UnaryOperator { + val node = frontend.newUnaryOperator("--", false, true) + node.input = this + + if (holder is StatementHolder) { + holder += node + } + + return node +} + +/** + * Creates a new [UnaryOperator] with a `++` prefix [UnaryOperator.operatorCode] in the Fluent Node + * DSL and invokes [ArgumentHolder.addArgument] of the nearest enclosing [ArgumentHolder]. + */ +context(frontend: LanguageFrontend<*, *>, holder: Holder) +fun Expression.incPrefix(): UnaryOperator { + val node = frontend.newUnaryOperator("++", false, true) node.input = this if (holder is StatementHolder) { @@ -1283,7 +1328,7 @@ operator fun Expression.inc(): UnaryOperator { */ context(frontend: LanguageFrontend<*, *>) fun Expression.incNoContext(): UnaryOperator { - val node = (frontend).newUnaryOperator("++", true, false) + val node = frontend.newUnaryOperator("++", true, false) node.input = this return node @@ -1295,11 +1340,11 @@ fun Expression.incNoContext(): UnaryOperator { */ context(frontend: LanguageFrontend<*, *>, holder: ArgumentHolder) infix fun Expression.eq(rhs: Expression): BinaryOperator { - val node = (frontend).newBinaryOperator("==") + val node = frontend.newBinaryOperator("==") node.lhs = this node.rhs = rhs - (holder) += node + holder += node return node } @@ -1310,11 +1355,11 @@ infix fun Expression.eq(rhs: Expression): BinaryOperator { */ context(frontend: LanguageFrontend<*, *>, holder: ArgumentHolder) infix fun Expression.gt(rhs: Expression): BinaryOperator { - val node = (frontend).newBinaryOperator(">") + val node = frontend.newBinaryOperator(">") node.lhs = this node.rhs = rhs - (holder) += node + holder += node return node } @@ -1325,11 +1370,11 @@ infix fun Expression.gt(rhs: Expression): BinaryOperator { */ context(frontend: LanguageFrontend<*, *>, holder: ArgumentHolder) infix fun Expression.ge(rhs: Expression): BinaryOperator { - val node = (frontend).newBinaryOperator(">=") + val node = frontend.newBinaryOperator(">=") node.lhs = this node.rhs = rhs - (holder) += node + holder += node return node } @@ -1340,7 +1385,7 @@ infix fun Expression.ge(rhs: Expression): BinaryOperator { */ context(frontend: LanguageFrontend<*, *>) infix fun Expression.lt(rhs: Expression): BinaryOperator { - val node = (frontend).newBinaryOperator("<") + val node = frontend.newBinaryOperator("<") node.lhs = this node.rhs = rhs @@ -1353,11 +1398,11 @@ infix fun Expression.lt(rhs: Expression): BinaryOperator { */ context(frontend: LanguageFrontend<*, *>, holder: ArgumentHolder) infix fun Expression.le(rhs: Expression): BinaryOperator { - val node = (frontend).newBinaryOperator("<=") + val node = frontend.newBinaryOperator("<=") node.lhs = this node.rhs = rhs - (holder) += node + holder += node return node } @@ -1372,10 +1417,10 @@ fun Expression.conditional( thenExpression: Expression, elseExpression: Expression, ): ConditionalExpression { - val node = (frontend).newConditionalExpression(condition, thenExpression, elseExpression) + val node = frontend.newConditionalExpression(condition, thenExpression, elseExpression) if (holder is StatementHolder) { - (holder) += node + holder += node } else if (holder is ArgumentHolder) { holder += node } @@ -1389,12 +1434,12 @@ fun Expression.conditional( */ context(frontend: LanguageFrontend<*, *>, holder: StatementHolder) infix fun Expression.assign(init: AssignExpression.() -> Expression): AssignExpression { - val node = (frontend).newAssignExpression("=") + val node = frontend.newAssignExpression("=") node.lhs = mutableListOf(this) init(node) // node.rhs = listOf(init(node)) - (holder) += node + holder += node return node } @@ -1405,7 +1450,7 @@ infix fun Expression.assign(init: AssignExpression.() -> Expression): AssignExpr */ context(frontend: LanguageFrontend<*, *>, holder: Holder) infix fun Expression.assign(rhs: Expression): AssignExpression { - val node = (frontend).newAssignExpression("=", listOf(this), listOf(rhs)) + val node = frontend.newAssignExpression("=", listOf(this), listOf(rhs)) if (holder is StatementHolder) { holder += node @@ -1420,7 +1465,67 @@ infix fun Expression.assign(rhs: Expression): AssignExpression { */ context(frontend: LanguageFrontend<*, *>, holder: Holder) infix fun Expression.assignPlus(rhs: Expression): AssignExpression { - val node = (frontend).newAssignExpression("+=", listOf(this), listOf(rhs)) + val node = frontend.newAssignExpression("+=", listOf(this), listOf(rhs)) + + if (holder is StatementHolder) { + holder += node + } + + return node +} + +/** + * Creates a new [AssignExpression] with a `-=` [AssignExpression.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. + */ +context(frontend: LanguageFrontend<*, *>, holder: Holder) +infix fun Expression.assignMinus(rhs: Expression): AssignExpression { + val node = frontend.newAssignExpression("-=", listOf(this), listOf(rhs)) + + if (holder is StatementHolder) { + holder += node + } + + return node +} + +/** + * Creates a new [AssignExpression] with a `*=` [AssignExpression.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. + */ +context(frontend: LanguageFrontend<*, *>, holder: Holder) +infix fun Expression.assignMult(rhs: Expression): AssignExpression { + val node = frontend.newAssignExpression("*=", listOf(this), listOf(rhs)) + + if (holder is StatementHolder) { + holder += node + } + + return node +} + +/** + * Creates a new [AssignExpression] with a `/=` [AssignExpression.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. + */ +context(frontend: LanguageFrontend<*, *>, holder: Holder) +infix fun Expression.assignDiv(rhs: Expression): AssignExpression { + val node = frontend.newAssignExpression("/=", listOf(this), listOf(rhs)) + + if (holder is StatementHolder) { + holder += node + } + + return node +} + +/** + * Creates a new [AssignExpression] with a `%=` [AssignExpression.operatorCode] in the Fluent Node + * DSL and adds it to the nearest enclosing [StatementHolder]. + */ +context(frontend: LanguageFrontend<*, *>, holder: Holder) +infix fun Expression.assignMod(rhs: Expression): AssignExpression { + val node = frontend.newAssignExpression("%=", listOf(this), listOf(rhs)) if (holder is StatementHolder) { holder += node @@ -1435,7 +1540,7 @@ infix fun Expression.assignPlus(rhs: Expression): AssignExpression { */ context(frontend: LanguageFrontend<*, *>, holder: Holder) infix fun Expression.assignAsExpr(rhs: Expression): AssignExpression { - val node = (frontend).newAssignExpression("=", listOf(this), listOf(rhs)) + val node = frontend.newAssignExpression("=", listOf(this), listOf(rhs)) node.usedAsExpression = true @@ -1448,7 +1553,7 @@ infix fun Expression.assignAsExpr(rhs: Expression): AssignExpression { */ context(frontend: LanguageFrontend<*, *>, holder: Holder) infix fun Expression.assignAsExpr(rhs: AssignExpression.() -> Unit): AssignExpression { - val node = (frontend).newAssignExpression("=", listOf(this)) + val node = frontend.newAssignExpression("=", listOf(this)) rhs(node) node.usedAsExpression = true @@ -1462,7 +1567,7 @@ infix fun Expression.assignAsExpr(rhs: AssignExpression.() -> Unit): AssignExpre */ context(frontend: LanguageFrontend<*, *>, holder: Holder) infix fun Expression.`throw`(init: (ThrowExpression.() -> Unit)?): ThrowExpression { - val node = (frontend).newThrowExpression() + val node = frontend.newThrowExpression() if (init != null) init(node) val holder = holder diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt index 07aced9a7a0..5bfe53578df 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt @@ -73,9 +73,10 @@ class PowersetLattice(override val elements: IdentitySet) : } /** - * Stores the current state. I.e., it maps [K] (e.g. a [Node] or [Edge]) to a [LatticeElement]. It - * provides some useful functions e.g. to check if the mapping has to be updated (e.g. because there - * are new nodes or because a new lattice element is bigger than the old one). + * Stores the current state. I.e., it maps [K] (e.g. a [Node] or [PropertyEdge]) to a + * [LatticeElement]. It provides some useful functions e.g. to check if the mapping has to be + * updated (e.g. because there are new nodes or because a new lattice element is bigger than the old + * one). */ open class State : HashMap>() { @@ -215,9 +216,6 @@ class Worklist() { return node } - /** Checks if [currentNode] has already been visited before. */ - fun hasAlreadySeen(currentNode: K) = currentNode in alreadySeen - /** Computes the meet over paths for all the states in [globalState]. */ fun mop(): State? { val firstKey = globalState.keys.firstOrNull() diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/functional/BasicLatticesRedesign.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/functional/BasicLatticesRedesign.kt index 7f936c6dafe..47046af858c 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/functional/BasicLatticesRedesign.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/functional/BasicLatticesRedesign.kt @@ -26,6 +26,7 @@ package de.fraunhofer.aisec.cpg.helpers.functional import de.fraunhofer.aisec.cpg.graph.edges.flows.EvaluationOrder +import de.fraunhofer.aisec.cpg.graph.statements.LoopStatement import de.fraunhofer.aisec.cpg.helpers.IdentitySet import de.fraunhofer.aisec.cpg.helpers.toIdentitySet import java.io.Serializable @@ -60,6 +61,30 @@ fun compareMultiple(vararg orders: Order) = else -> Order.UNEQUAL } +interface HasWidening { + /** + * Computes the widening of [one] and [two]. This is used to ensure that the fixpoint iteration + * converges (faster). + * + * @param one The first element to widen + * @param two The second element to widen + * @return The widened element + */ + fun widen(one: T, two: T): T +} + +interface HasNarrowing { + /** + * Computes the narrowing of [one] and [two]. This is used to ensure that the fixpoint iteration + * converges (faster) without too much overapproximation. + * + * @param one The first element to narrow + * @param two The second element to narrow + * @return The narrowed element + */ + fun narrow(one: T, two: T): T +} + /** * A lattice is a partially ordered structure of values of type [T]. [T] could be anything, where * common examples are sets, ranges, maps, tuples, but it can also have random names and a new data @@ -78,6 +103,13 @@ fun compareMultiple(vararg orders: Order) = * and currently has no real effect. */ interface Lattice { + enum class Strategy { + PRECISE, + WIDENING, + WIDENING_NARROWING, + NARROWING, + } + /** * Represents a single element of the [Lattice]. It also provides the functionality to compare * and duplicate the element. @@ -106,7 +138,7 @@ interface Lattice { * is modified if there is no element greater than each other (if set to `true`) or if a new * [Lattice.Element] is returned (if set to `false`). */ - fun lub(one: T, two: T, allowModify: Boolean = false): T + fun lub(one: T, two: T, allowModify: Boolean = false, widen: Boolean = false): T /** Computes the greatest lower bound (meet) of [one] and [two] */ fun glb(one: T, two: T): T @@ -137,6 +169,7 @@ interface Lattice { startEdges: List, startState: T, transformation: (Lattice, EvaluationOrder, T) -> T, + strategy: Strategy = Strategy.PRECISE, ): T { val globalState = IdentityHashMap() var finalState: T = this.bottom @@ -212,9 +245,35 @@ interface Lattice { // the state in comparison to the previous time we were there. val oldGlobalIt = globalState[it] + + // If we're on the loop head (some node is LoopStatement), and we use WIDENING or + // WIDENING_NARROWING, we have to apply the widening/narrowing here (if oldGlobalIt + // is not null). val newGlobalIt = - (oldGlobalIt?.let { this.lub(newState, it, isNotNearStartOrEndOfBasicBlock) } - ?: newState) + if ( + nextEdge.end is LoopStatement && + (strategy == Strategy.WIDENING || + strategy == Strategy.WIDENING_NARROWING) && + oldGlobalIt != null + ) { + this.lub( + one = newState, + two = oldGlobalIt, + allowModify = isNotNearStartOrEndOfBasicBlock, + widen = true, + ) + } else if (strategy == Strategy.NARROWING) { + TODO() + } else { + (oldGlobalIt?.let { + this.lub( + one = newState, + two = it, + allowModify = isNotNearStartOrEndOfBasicBlock, + ) + } ?: newState) + } + globalState[it] = newGlobalIt if ( it !in currentBBEdgesList && @@ -222,7 +281,8 @@ interface Lattice { it !in mergePointsEdgesList && (isNoBranchingPoint || oldGlobalIt == null || - newGlobalIt.compare(oldGlobalIt) == Order.GREATER) + newGlobalIt.compare(oldGlobalIt) == Order.GREATER || + newGlobalIt.compare(oldGlobalIt) == Order.UNEQUAL) ) { if (it.start.prevEOGEdges.size > 1) { // This edge brings us to a merge point, so we add it to the list of merge @@ -324,7 +384,12 @@ class PowersetLattice() : Lattice> { override val bottom: Element get() = Element() - override fun lub(one: Element, two: Element, allowModify: Boolean): Element { + override fun lub( + one: Element, + two: Element, + allowModify: Boolean, + widen: Boolean, + ): Element { if (allowModify) { one += two return one @@ -357,7 +422,7 @@ open class MapLattice(val innerLattice: Lattice) : Lattice> { override lateinit var elements: Set> - class Element(expectedMaxSize: Int) : + open class Element(expectedMaxSize: Int) : IdentityHashMap(expectedMaxSize), Lattice.Element { constructor() : this(32) @@ -450,7 +515,12 @@ open class MapLattice(val innerLattice: Lattice) : override val bottom: Element get() = MapLattice.Element() - override fun lub(one: Element, two: Element, allowModify: Boolean): Element { + override fun lub( + one: Element, + two: Element, + allowModify: Boolean, + widen: Boolean, + ): Element { if (allowModify) { two.forEach { (k, v) -> if (!one.containsKey(k)) { @@ -458,12 +528,13 @@ open class MapLattice(val innerLattice: Lattice) : one[k] = v } else { // This key already exists in "one", so we have to compute the lub of the values - one[k]?.let { oneValue -> innerLattice.lub(oneValue, v, true) } + one[k]?.let { oneValue -> + innerLattice.lub(one = oneValue, two = v, allowModify = true, widen = widen) + } } } return one } - val allKeys = one.keys.toIdentitySet() allKeys += two.keys val newMap = @@ -472,7 +543,12 @@ open class MapLattice(val innerLattice: Lattice) : val thisValue = one[key] val newValue = if (thisValue != null && otherValue != null) { - innerLattice.lub(thisValue, otherValue, allowModify) + innerLattice.lub( + one = thisValue, + two = otherValue, + allowModify = false, + widen = widen, + ) } else thisValue ?: otherValue newValue?.let { current[key] = it } current @@ -509,13 +585,13 @@ open class MapLattice(val innerLattice: Lattice) : * Implements the [Lattice] for a lattice over two other lattices which are represented by * [innerLattice1] and [innerLattice2]. */ -class TupleLattice( +open class TupleLattice( val innerLattice1: Lattice, val innerLattice2: Lattice, ) : Lattice> { override lateinit var elements: Set> - class Element(val first: S, val second: T) : + open class Element(val first: S, val second: T) : Serializable, Lattice.Element { override fun toString(): String = "($first, $second)" @@ -555,15 +631,30 @@ class TupleLattice( override val bottom: Element get() = Element(innerLattice1.bottom, innerLattice2.bottom) - override fun lub(one: Element, two: Element, allowModify: Boolean): Element { + override fun lub( + one: Element, + two: Element, + allowModify: Boolean, + widen: Boolean, + ): Element { return if (allowModify) { - innerLattice1.lub(one.first, two.first, true) - innerLattice2.lub(one.second, two.second, true) + innerLattice1.lub(one = one.first, two = two.first, allowModify = true, widen = widen) + innerLattice2.lub(one = one.second, two = two.second, allowModify = true, widen = widen) one } else { Element( - innerLattice1.lub(one.first, two.first), - innerLattice2.lub(one.second, two.second), + innerLattice1.lub( + one = one.first, + two = two.first, + allowModify = false, + widen = widen, + ), + innerLattice2.lub( + one = one.second, + two = two.second, + allowModify = false, + widen = widen, + ), ) } } @@ -642,17 +733,33 @@ class TripleLattice, two: Element, allowModify: Boolean, + widen: Boolean, ): Element { return if (allowModify) { - innerLattice1.lub(one.first, two.first, true) - innerLattice2.lub(one.second, two.second, true) - innerLattice3.lub(one.third, two.third, true) + innerLattice1.lub(one = one.first, two = two.first, allowModify = true, widen = widen) + innerLattice2.lub(one = one.second, two = two.second, allowModify = true, widen = widen) + innerLattice3.lub(one = one.third, two = two.third, allowModify = true, widen = widen) one } else { Element( - innerLattice1.lub(one.first, two.first), - innerLattice2.lub(one.second, two.second), - innerLattice3.lub(one.third, two.third), + innerLattice1.lub( + one = one.first, + two = two.first, + allowModify = false, + widen = widen, + ), + innerLattice2.lub( + one = one.second, + two = two.second, + allowModify = false, + widen = widen, + ), + innerLattice3.lub( + one = one.third, + two = two.third, + allowModify = false, + widen = widen, + ), ) } } From b8ff7093be6a46b3f4bda9aba6586df61f9d7cfc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 08:36:26 +0200 Subject: [PATCH 15/93] Update dependency @sveltejs/kit to v2.44.0 (#2441) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 794c6d9e883..36a0fac67a2 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -23,10 +23,10 @@ importers: version: 5.2.7 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.8(@sveltejs/kit@2.43.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) + version: 3.0.8(@sveltejs/kit@2.44.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) '@sveltejs/kit': specifier: ^2.20.6 - version: 2.43.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 2.44.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 version: 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) @@ -502,8 +502,8 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.43.5': - resolution: {integrity: sha512-44Mm5csR4mesKx2Eyhtk8UVrLJ4c04BT2wMTfYGKJMOkUqpHP5KLL2DPV0hXUA4t4+T3ZYe0aBygd42lVYv2cA==} + '@sveltejs/kit@2.44.0': + resolution: {integrity: sha512-xU5qP7PiYmrSH70Whm/I+nf0j4xBnHyRQNkC1SEfaBOwCCkkeuL6WNxSb8q4Ib7+Z+sZ4JUTDYHfoyVm02EXVQ==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -1841,11 +1841,11 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.43.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.44.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.43.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.44.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) - '@sveltejs/kit@2.43.5(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/kit@2.44.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) From 0d39b4c40cc1e0564cb5600b9267eaa68a07562c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 06:47:35 +0000 Subject: [PATCH 16/93] Update dependency com.charleskorn.kaml:kaml to v0.97.0 (#2442) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b20977bccd2..1616efef2e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ publish = "0.34.0" sootup = "2.0.0" slf4j = "2.0.16" clikt = "5.0.2" -kaml = "0.96.0" +kaml = "0.97.0" sarif4k = "0.6.0" ktor = "3.3.0" node = "22.14.0" From 24177692eafa923b60ae89b8826a048fd04dda17 Mon Sep 17 00:00:00 2001 From: KuechA <31155350+KuechA@users.noreply.github.com> Date: Tue, 7 Oct 2025 00:11:16 +0200 Subject: [PATCH 17/93] Timeout for CDG and CFSensitiveDFG pass (#2417) * Timeout for CDG and CFSensitiveDFG pass * Make the timeout an argument of iterateEOG * remove existing dfg edges only on success * apply strategy usage to to timeoutless invocation * updating call due to having non positional argument * Returning a non null time to solve the issue because apparently we are able to * Adding Test. Fixing coding issue that actually reran the iteration without timeout if the iteration timeouted * Add nullness conditional checks * Fixing coding issue that actually reran the iteration without timeout if the iteration timeouted * Spotless --------- Co-authored-by: Konrad Weiss --- .../abstracteval/AbstractIntervalEvaluator.kt | 2 +- .../aisec/cpg/passes/UnreachableEOGPass.kt | 10 +++- .../cpg/passes/concepts/EOGConceptPass.kt | 5 +- cpg-core/build.gradle.kts | 1 + .../aisec/cpg/helpers/EOGWorklist.kt | 44 ++++++++++++-- .../functional/BasicLatticesRedesign.kt | 25 +++++++- .../cpg/passes/ControlDependenceGraphPass.kt | 24 +++++++- .../cpg/passes/ControlFlowSensitiveDFGPass.kt | 57 ++++++++++++++++--- .../cpg/passes/SymbolResolverEOGIteration.kt | 7 ++- .../passes/ControlDependenceGraphPassTest.kt | 45 +++++++++++++++ gradle/libs.versions.toml | 1 + 11 files changed, 201 insertions(+), 20 deletions(-) diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractIntervalEvaluator.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractIntervalEvaluator.kt index 155e7df854e..cadb50fb414 100644 --- a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractIntervalEvaluator.kt +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/abstracteval/AbstractIntervalEvaluator.kt @@ -411,7 +411,7 @@ class AbstractIntervalEvaluator { ::handleNode, strategy = Lattice.Strategy.WIDENING_NARROWING, ) - return finalState.second.get(targetNode)?.element ?: LatticeInterval.BOTTOM + return finalState?.second?.get(targetNode)?.element ?: LatticeInterval.BOTTOM } /** diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/UnreachableEOGPass.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/UnreachableEOGPass.kt index c9f932be2f1..37f233b28f2 100644 --- a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/UnreachableEOGPass.kt +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/UnreachableEOGPass.kt @@ -75,7 +75,15 @@ open class UnreachableEOGPass(ctx: TranslationContext) : EOGStarterPass(ctx) { } val nextEog = node.nextEOGEdges.toList() - val finalStateNew = unreachabilityState.iterateEOG(nextEog, startState, ::transfer) + val finalStateNew = + unreachabilityState.iterateEOG(nextEog, startState, ::transfer) + ?: run { + log.warn( + "Could not compute unreachability of EOG edges for {}, reached a timeout", + node.name, + ) + return@handle + } for ((key, value) in finalStateNew) { if (value.reachability == Reachability.UNREACHABLE) { diff --git a/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/concepts/EOGConceptPass.kt b/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/concepts/EOGConceptPass.kt index 32bf2d0bf29..6486380feb0 100644 --- a/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/concepts/EOGConceptPass.kt +++ b/cpg-concepts/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/concepts/EOGConceptPass.kt @@ -87,7 +87,10 @@ open class EOGConceptPass(ctx: TranslationContext) : val nextEog = node.nextEOGEdges.toList() val finalState = - lattice.lub(lattice.iterateEOG(nextEog, startState, ::transfer), startState, true) + lattice.iterateEOG(nextEog, startState, ::transfer)?.let { tmpFinalState -> + lattice.lub(tmpFinalState, startState, true) + } ?: startState + // We set the underlying node based on the final state for ((underlyingNode, overlayNodes) in finalState) { overlayNodes.forEach { diff --git a/cpg-core/build.gradle.kts b/cpg-core/build.gradle.kts index da734540ab1..85e5cf7afe3 100644 --- a/cpg-core/build.gradle.kts +++ b/cpg-core/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(libs.bundles.log4j) implementation(libs.kotlin.reflect) implementation(libs.jacksonyml) + implementation(libs.kotlinx.coroutines.core) testImplementation(libs.junit.params) diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt index 5bfe53578df..c27b27a05b9 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/EOGWorklist.kt @@ -28,6 +28,8 @@ package de.fraunhofer.aisec.cpg.helpers import de.fraunhofer.aisec.cpg.graph.Node import de.fraunhofer.aisec.cpg.graph.edges.Edge import java.util.IdentityHashMap +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull /** * A complete lattice is an ordered structure of values of type [T]. [T] could be anything, e.g., a @@ -240,9 +242,10 @@ class Worklist() { inline fun iterateEOG( startNode: K, startState: State, - transformation: (K, State) -> State, + timeout: Long? = null, + crossinline transformation: (K, State) -> State, ): State? { - return iterateEOG(startNode, startState) { k, s, _ -> transformation(k, s) } + return iterateEOG(startNode, startState, timeout) { k, s, _ -> transformation(k, s) } } /** @@ -256,6 +259,21 @@ inline fun iterateEOG( * not useful for this analysis. The [transformation] has to return the updated [State]. */ inline fun iterateEOG( + startNode: K, + startState: State, + timeout: Long? = null, + crossinline transformation: (K, State, Worklist) -> State, +): State? { + return runBlocking { + if (timeout != null) { + withTimeoutOrNull(timeout) { iterateEogInternal(startNode, startState, transformation) } + } else { + iterateEogInternal(startNode, startState, transformation) + } + } +} + +inline fun iterateEogInternal( startNode: K, startState: State, transformation: (K, State, Worklist) -> State, @@ -292,15 +310,31 @@ inline fun iterateEOG( inline fun , N : Any, V> iterateEOG( startEdges: List, startState: State, - transformation: (K, State) -> State, + timeout: Long? = null, + crossinline transformation: (K, State) -> State, ): State? { - return iterateEOG(startEdges, startState) { k, s, _ -> transformation(k, s) } + return iterateEOG(startEdges, startState, timeout) { k, s, _ -> transformation(k, s) } } inline fun , N : Any, V> iterateEOG( startEdges: List, startState: State, - transformation: (K, State, Worklist) -> State, + timeout: Long? = null, + crossinline transformation: (K, State, Worklist) -> State, +): State? { + return runBlocking { + timeout?.let { timeout -> + withTimeoutOrNull(timeout) { + iterateEogInternal(startEdges, startState, transformation) + } + } ?: run { iterateEogInternal(startEdges, startState, transformation) } + } +} + +inline fun , N : Any, V> iterateEogInternal( + startEdges: List, + startState: State, + crossinline transformation: (K, State, Worklist) -> State, ): State? { val globalState = IdentityHashMap>() for (startEdge in startEdges) { diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/functional/BasicLatticesRedesign.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/functional/BasicLatticesRedesign.kt index 47046af858c..80826dfd79e 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/functional/BasicLatticesRedesign.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/functional/BasicLatticesRedesign.kt @@ -37,6 +37,8 @@ import kotlin.collections.fold import kotlin.collections.plusAssign import kotlin.collections.set import kotlin.math.ceil +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull /** Used to identify the order of elements */ enum class Order { @@ -163,13 +165,34 @@ interface Lattice { * Computes a fixpoint by iterating over the EOG beginning with the [startEdges] and a state * [startState]. This means, it keeps applying [transformation] until the state does no longer * change. With state, we mean a mapping between the [EvaluationOrder] edges to the value of - * [Lattice] which represents possible values (or abstractions thereof) that they hold. + * [Lattice] which represents possible values (or abstractions thereof) that they hold. The + * [timeout] can be used to limit the time spent in this function. If the timeout is reached and + * the fixpoint is not reached yet, we return `null`. If [timeout] is `null`, we will not time + * out. */ fun iterateEOG( startEdges: List, startState: T, transformation: (Lattice, EvaluationOrder, T) -> T, strategy: Strategy = Strategy.PRECISE, + timeout: Long? = null, + ): T? { + return runBlocking { + if (timeout != null) { + withTimeoutOrNull(timeout) { + iterateEogInternal(startEdges, startState, transformation, strategy) + } + } else { + iterateEogInternal(startEdges, startState, transformation, strategy) + } + } + } + + private fun iterateEogInternal( + startEdges: List, + startState: T, + transformation: (Lattice, EvaluationOrder, T) -> T, + strategy: Strategy, ): T { val globalState = IdentityHashMap() var finalState: T = this.bottom diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlDependenceGraphPass.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlDependenceGraphPass.kt index 61218fe7299..993360e0264 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlDependenceGraphPass.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlDependenceGraphPass.kt @@ -56,7 +56,13 @@ open class ControlDependenceGraphPass(ctx: TranslationContext) : EOGStarterPass( * [de.fraunhofer.aisec.cpg.graph.statements.Statement.cyclomaticComplexity]) a * [FunctionDeclaration] must have in order to be considered. */ - var maxComplexity: Int? = null + var maxComplexity: Int? = null, + /** + * This specifies the maximum time (in ms) we want to spend analyzing a single + * [de.fraunhofer.aisec.cpg.graph.EOGStarterHolder]. If the time is exceeded, we skip the + * function (or whatever is starting the EOG). If `null`, no time limit is enforced. + */ + var timeout: Long? = null, ) : PassConfiguration() override fun cleanup() { @@ -80,6 +86,7 @@ open class ControlDependenceGraphPass(ctx: TranslationContext) : EOGStarterPass( if (startNode !is FunctionDeclaration) { return } + val max = passConfig()?.maxComplexity val c = startNode.body?.cyclomaticComplexity ?: 0 if (max != null && c > max) { @@ -112,7 +119,20 @@ open class ControlDependenceGraphPass(ctx: TranslationContext) : EOGStarterPass( ) log.trace("Iterating EOG of {}", firstBasicBlock) val finalState = - prevEOGState.iterateEOG(firstBasicBlock.nextEOGEdges, startState, ::transfer) + prevEOGState.iterateEOG( + firstBasicBlock.nextEOGEdges, + startState, + ::transfer, + timeout = passConfig()?.timeout, + ) + ?: run { + log.warn( + "Timeout while computing CDG for {}, skipping CDG generation", + startNode.name, + ) + return@accept + } + log.trace("Done iterating EOG for {}. Generating the edges now.", startNode.name) // branchingNodeConditionals is a map organized as follows: diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlFlowSensitiveDFGPass.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlFlowSensitiveDFGPass.kt index 1fc7054dbcd..798085feb12 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlFlowSensitiveDFGPass.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlFlowSensitiveDFGPass.kt @@ -56,13 +56,21 @@ open class ControlFlowSensitiveDFGPass(ctx: TranslationContext) : EOGStarterPass * [Statement.cyclomaticComplexity]) a [FunctionDeclaration] must have in order to be * considered. */ - var maxComplexity: Int? = null + var maxComplexity: Int? = null, + /** + * This specifies the maximum time (in ms) we want to spend analyzing a single + * [de.fraunhofer.aisec.cpg.graph.EOGStarterHolder]. If the time is exceeded, we skip the + * function (or whatever is starting the EOG). If `null`, no time limit is enforced. + */ + var timeout: Long? = null, ) : PassConfiguration() override fun cleanup() { // Nothing to do } + var purelyLocalNodes: Set = setOf() + /** We perform the actions for each [FunctionDeclaration]. */ override fun accept(node: Node) { if (node is FunctionDeclaration && node.body == null) { @@ -75,7 +83,6 @@ open class ControlFlowSensitiveDFGPass(ctx: TranslationContext) : EOGStarterPass // These are EOGStarterHolders but do not have an EOG which means, they will just cause // problems. Again, if we delete information/edges, we will never be able to recover them. if (node is FunctionTemplateDeclaration) return - // Calculate the complexity of the function and see, if it exceeds our threshold val max = passConfig()?.maxComplexity val c = (node as? FunctionDeclaration)?.body?.cyclomaticComplexity ?: 0 @@ -88,8 +95,14 @@ open class ControlFlowSensitiveDFGPass(ctx: TranslationContext) : EOGStarterPass log.trace("Handling {} (complexity: {})", node.name, c) + var edgesToRemove: Set = setOf() + var allNodesWithEdgesToRemove: Set = setOf() + if (node is AstNode) { - clearFlowsOfVariableDeclarations(node) + val tmpTriple = collectFlowsToClear(node) + edgesToRemove = tmpTriple.first + allNodesWithEdgesToRemove = tmpTriple.second + purelyLocalNodes = tmpTriple.third } val startState = DFGPassState>() @@ -113,7 +126,18 @@ open class ControlFlowSensitiveDFGPass(ctx: TranslationContext) : EOGStarterPass } val finalState = - iterateEOG(node.nextEOGEdges, startState, ::transfer) as? DFGPassState ?: return + iterateEOG( + node.nextEOGEdges, + startState, + passConfig()?.timeout, + ::transfer, + ) + as? DFGPassState ?: return + + for (node in allNodesWithEdgesToRemove) { + node.prevDFGEdges.removeAll(edgesToRemove) + node.nextDFGEdges.removeAll(edgesToRemove) + } removeUnreachableImplicitReturnStatement( node, @@ -168,7 +192,7 @@ open class ControlFlowSensitiveDFGPass(ctx: TranslationContext) : EOGStarterPass * Removes all the incoming and outgoing DFG edges for each variable declaration in the block of * code [node]. */ - protected fun clearFlowsOfVariableDeclarations(node: AstNode) { + protected fun collectFlowsToClear(node: AstNode): Triple, Set, Set> { // Get all children of the node which are not part of child EOG starters' children. We need // this to filter out effects on the childStarters' children. We do not want to impact them, // so we later filter out all things which occur in the children or even completely outside @@ -183,6 +207,10 @@ open class ControlFlowSensitiveDFGPass(ctx: TranslationContext) : EOGStarterPass } ) + val edgesToRemove = mutableSetOf() + val allNodesWithEdgesToRemove = mutableSetOf() + val purelyLocalNodes = mutableSetOf() + // Get the local variables and parameters inside the node's astChildren (without the // childStarters' children). For these, we remove prev and next DFG edges from/to nodes // inside the node's astChildren @@ -193,15 +221,28 @@ open class ControlFlowSensitiveDFGPass(ctx: TranslationContext) : EOGStarterPass it !is FieldDeclaration && it !is TupleDeclaration) || it is ParameterDeclaration }) { + allNodesWithEdgesToRemove.add(varDecl) // Clear only prev DFG inside this function! varDecl.prevDFGEdges .filter { it.start in allChildrenOfFunction } - .forEach { varDecl.prevDFGEdges.remove(it) } + .forEach { + edgesToRemove.add(it) + // varDecl.prevDFGEdges.remove(it) + } // Clear only next DFG inside this function! varDecl.nextDFGEdges .filter { it.end in allChildrenOfFunction } - .forEach { varDecl.nextDFGEdges.remove(it) } + .forEach { + edgesToRemove.add(it) + // varDecl.nextDFGEdges.remove(it) + } + if ( + varDecl.prevDFGEdges.all { it in edgesToRemove } and + varDecl.nextDFGEdges.all { it in edgesToRemove } + ) + purelyLocalNodes.add(varDecl) } + return Triple(edgesToRemove, allNodesWithEdgesToRemove, purelyLocalNodes) } /** @@ -476,7 +517,7 @@ open class ControlFlowSensitiveDFGPass(ctx: TranslationContext) : EOGStarterPass // DFG edges left which are associated to this variable declaration. In this case, we // add the write operation from this reference to the variable declaration. currentNode.refersTo?.let { variableDecl -> - if (variableDecl.prevDFG.isNotEmpty() || variableDecl.nextDFG.isNotEmpty()) + if (variableDecl !in purelyLocalNodes) doubleState.push(variableDecl, PowersetLattice(identitySetOf(currentNode))) } } else { diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolverEOGIteration.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolverEOGIteration.kt index e5527862bc6..9e3a7f9247e 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolverEOGIteration.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolverEOGIteration.kt @@ -213,7 +213,12 @@ fun SymbolResolver.acceptWithIterateEOG(t: Node) { } t.scope?.let { startState = startState.pushDeclarationToScope(lattice, it) } - val finalState = lattice.iterateEOG(t.nextEOGEdges, startState, ::transfer) + val finalState = + lattice.iterateEOG(t.nextEOGEdges, startState, ::transfer) + ?: run { + log.warn("Could not compute final state for function {} (due to timeout)", t.name) + return@acceptWithIterateEOG + } finalState.candidates.forEach { node, candidates -> if (node is Reference) { diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/ControlDependenceGraphPassTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/ControlDependenceGraphPassTest.kt index 59371be0757..2ebaf50796a 100644 --- a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/ControlDependenceGraphPassTest.kt +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/ControlDependenceGraphPassTest.kt @@ -111,6 +111,12 @@ class ControlDependenceGraphPassTest { assertFalse(forEachStmt in printAfterLoop.prevCDG) } + @Test + fun testTimoutEffective() { + val result = getTimeoutTest() + assertTrue { result.allChildren().flatMap { it.nextCDG }.isEmpty() } + } + companion object { fun getIfTest() = testFrontend( @@ -181,5 +187,44 @@ class ControlDependenceGraphPassTest { } } } + + fun getTimeoutTest() = + testFrontend( + TranslationConfiguration.builder() + .registerLanguage() + .defaultPasses() + .registerPass() + .configurePass( + ControlDependenceGraphPass.Configuration(timeout = 0L) + ) + .build() + ) + .build { + translationResult { + translationUnit("if.cpp") { + // The main method + function("main", t("int")) { + body { + declare { variable("i", t("int")) { literal(0, t("int")) } } + ifStmt { + condition { ref("i") lt literal(1, t("int")) } + thenStmt { + ref("i") assign literal(1, t("int")) + call("printf") { literal("0\n", t("string")) } + } + } + call("printf") { literal("1\n", t("string")) } + ifStmt { + condition { ref("i") gt literal(0, t("int")) } + thenStmt { ref("i") assign literal(2, t("int")) } + elseStmt { ref("i") assign literal(3, t("int")) } + } + call("printf") { literal("2\n", t("string")) } + returnStmt { ref("i") } + } + } + } + } + } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1616efef2e0..b6de63fccb3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ kotlin-scripting-jvm-host = { module = "org.jetbrains.kotlin:kotlin-scripting-jv kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin"} # this is only needed for the testFixtures in cpg-core, everywhere else kotlin("test") is used kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-json" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.10.2"} reflections = { module = "org.reflections:reflections", version = "0.10.2"} From ddda6cd508b9fa4b485f0de1c2198e9d9e3046fd Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 8 Oct 2025 09:39:24 +0200 Subject: [PATCH 18/93] Only put AST nodes in `fields`/`methods`/... in `RecordDeclaration` that are part of the AST (#2403) * Added API to return methods of a Type * Added members, fields and methods * Add `declaringScope` to `Declaration`. * Extended unit test * Rename fields/methods/... in `RecordDeclaration` to `innerFields`, ... This should make it clear that it only contains those that are directly embedded into the AST structure. * Do not add Go's method to innerMethod AST property anymore * Fixed Go tests by using the correct Type API * Do not add outside methods in C++ to record inner methods * Trying to get rid of addXXX functions * Fixed test * Removed INNER_ --- .../fraunhofer/aisec/cpg/graph/TypeBuilder.kt | 2 +- .../aisec/cpg/graph/builder/Fluent.kt | 4 +- .../graph/declarations/RecordDeclaration.kt | 67 +++++++++---------- .../aisec/cpg/graph/types/ObjectType.kt | 6 +- .../aisec/cpg/passes/SymbolResolver.kt | 7 +- .../aisec/cpg/passes/TypeHierarchyResolver.kt | 1 - .../aisec/cpg/passes/inference/Inference.kt | 11 +-- .../fraunhofer/aisec/cpg/graph/WalkerTest.kt | 4 +- .../aisec/cpg/graph/edges/EdgesTest.kt | 4 +- .../aisec/cpg/processing/VisitorTest.kt | 1 - .../cpg/frontends/cxx/DeclaratorHandler.kt | 48 +++++-------- .../frontends/golang/DeclarationHandler.kt | 11 +-- .../frontends/golang/SpecificationHandler.kt | 4 +- .../aisec/cpg/passes/GoExtraPass.kt | 2 +- .../cpg/frontends/golang/DeclarationTest.kt | 12 +++- .../golang/GoLanguageFrontendTest.kt | 6 +- .../aisec/cpg/passes/JavaImportResolver.kt | 4 +- 17 files changed, 83 insertions(+), 111 deletions(-) diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/TypeBuilder.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/TypeBuilder.kt index d88febb7ac9..7478c45011a 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/TypeBuilder.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/TypeBuilder.kt @@ -94,7 +94,7 @@ fun LanguageProvider.objectType( // Otherwise, we either need to create the type because of the generics or because we do not // know the type yet. - var type = + val type = ObjectType( typeName = name, generics = generics, diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt index 149e9d74c0d..d45b032f353 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt @@ -217,7 +217,7 @@ fun LanguageFrontend<*, *>.method( scopeManager.leaveScope(node) scopeManager.addDeclaration(node) - record.addMethod(node) + record.methods += node return node } @@ -239,7 +239,7 @@ fun LanguageFrontend<*, *>.constructor( scopeManager.leaveScope(node) scopeManager.addDeclaration(node) - recordDeclaration.addConstructor(node) + recordDeclaration.constructors += node return node } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/declarations/RecordDeclaration.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/declarations/RecordDeclaration.kt index 4857b3c845a..9d967242ecc 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/declarations/RecordDeclaration.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/declarations/RecordDeclaration.kt @@ -25,6 +25,7 @@ */ package de.fraunhofer.aisec.cpg.graph.declarations +import de.fraunhofer.aisec.cpg.frontends.TranslationException import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.edges.Edge.Companion.propertyEqualsList import de.fraunhofer.aisec.cpg.graph.edges.ast.astEdgesOf @@ -50,20 +51,44 @@ open class RecordDeclaration : /** The kind, i.e. struct, class, union or enum. */ var kind: String? = null + /** + * The [FieldDeclaration]s that are directly contained in this record declaration's AST + * structure. This does not include any fields that might be declared in a base class or + * interface or fields that are declared outside the AST structure. + */ @Relationship(value = "FIELDS", direction = Relationship.Direction.OUTGOING) var fieldEdges = astEdgesOf() + /** Virtual property to directly access the nodes in [fieldEdges]. */ var fields by unwrapping(RecordDeclaration::fieldEdges) + /** + * The [MethodDeclaration]s that are directly contained in this record declaration's AST + * structure. This does not include any methods that might be declared in a base class or + * interface or methods that are declared outside the AST structure. + */ @Relationship(value = "METHODS", direction = Relationship.Direction.OUTGOING) var methodEdges = astEdgesOf() + /** Virtual property to directly access the nodes in [methods]. */ var methods by unwrapping(RecordDeclaration::methodEdges) + /** + * The [ConstructorDeclaration]s that are directly contained in this record declaration's AST + * structure. This does not include any constructors that might be declared in a base class or + * interface or constructors that are declared outside the AST structure. + */ @Relationship(value = "CONSTRUCTORS", direction = Relationship.Direction.OUTGOING) var constructorEdges = astEdgesOf() + /** Virtual property to directly access the nodes in [constructors]. */ var constructors by unwrapping(RecordDeclaration::constructorEdges) + /** + * The [RecordDeclaration]s that are directly contained in this record declaration's AST + * structure. This does not include any records that might be declared in a base class or + * interface or records that are declared outside the AST structure. + */ @Relationship(value = "RECORDS", direction = Relationship.Direction.OUTGOING) var recordEdges = astEdgesOf() + /** Virtual property to directly access the nodes in [recordEdges]. */ var records by unwrapping(RecordDeclaration::recordEdges) @Relationship(value = "TEMPLATES", direction = Relationship.Direction.OUTGOING) @@ -95,38 +120,6 @@ open class RecordDeclaration : @Relationship var staticImports: MutableSet = HashSet() - fun addField(fieldDeclaration: FieldDeclaration) { - addIfNotContains(fieldEdges, fieldDeclaration) - } - - fun removeField(fieldDeclaration: FieldDeclaration) { - fieldEdges.removeIf { it.end == fieldDeclaration } - } - - fun addMethod(methodDeclaration: MethodDeclaration) { - addIfNotContains(methodEdges, methodDeclaration) - } - - fun removeMethod(methodDeclaration: MethodDeclaration?) { - methodEdges.removeIf { it.end == methodDeclaration } - } - - fun addConstructor(constructorDeclaration: ConstructorDeclaration) { - addIfNotContains(constructorEdges, constructorDeclaration) - } - - fun removeConstructor(constructorDeclaration: ConstructorDeclaration?) { - constructorEdges.removeIf { it.end == constructorDeclaration } - } - - fun removeRecord(recordDeclaration: RecordDeclaration) { - recordEdges.removeIf { it.end == recordDeclaration } - } - - fun removeTemplate(templateDeclaration: TemplateDeclaration?) { - templateEdges.removeIf { it.end == templateDeclaration } - } - @DoNotPersist override val declarations: List get() { @@ -217,16 +210,18 @@ open class RecordDeclaration : * * @return the type */ - fun toType(): Type { + fun toType(): ObjectType { val type = objectType(name) if (type is ObjectType) { - // as a shortcut, directly set the record declaration. This will be otherwise done + // As a shortcut, directly set the record declaration. This will be otherwise done // later by a pass, but for some frontends we need this immediately, so we set // this here. type.recordDeclaration = this + type.superTypes.addAll(this.superTypes) + return type + } else { + throw TranslationException("Cannot create type for $this, as it is not an ObjectType") } - type.superTypes.addAll(this.superTypes) - return type } override val declaredType: Type diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/types/ObjectType.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/types/ObjectType.kt index cec8bc1c8e0..f2820e43186 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/types/ObjectType.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/types/ObjectType.kt @@ -27,11 +27,7 @@ package de.fraunhofer.aisec.cpg.graph.types import de.fraunhofer.aisec.cpg.PopulatedByPass import de.fraunhofer.aisec.cpg.frontends.Language -import de.fraunhofer.aisec.cpg.graph.declarations.ConstructorDeclaration -import de.fraunhofer.aisec.cpg.graph.declarations.Declaration -import de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration -import de.fraunhofer.aisec.cpg.graph.declarations.MethodDeclaration -import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.* import de.fraunhofer.aisec.cpg.graph.scopes.Scope import de.fraunhofer.aisec.cpg.graph.types.PointerType.PointerOrigin import de.fraunhofer.aisec.cpg.graph.unknownType diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt index 03dfd76173e..6e4558a1230 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt @@ -688,7 +688,12 @@ open class SymbolResolver(ctx: TranslationContext) : EOGStarterPass(ctx) { ): Set { return declaration.overriddenBy .filter { f -> - f is MethodDeclaration && f.recordDeclaration?.toType() in possibleSubTypes + if (f is MethodDeclaration) { + val record = f.recordDeclaration + record != null && record.toType() in possibleSubTypes + } else { + false + } } .toSet() } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/TypeHierarchyResolver.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/TypeHierarchyResolver.kt index b7d188e48a2..b1b314621a9 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/TypeHierarchyResolver.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/TypeHierarchyResolver.kt @@ -37,7 +37,6 @@ import de.fraunhofer.aisec.cpg.graph.types.ObjectType import de.fraunhofer.aisec.cpg.passes.configuration.DependsOn import de.fraunhofer.aisec.cpg.processing.IVisitor import de.fraunhofer.aisec.cpg.processing.strategy.Strategy -import java.util.* /** * Transitively connect [RecordDeclaration] nodes with their supertypes' records. diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/inference/Inference.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/inference/Inference.kt index 1b154d47755..6c18443831f 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/inference/Inference.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/inference/Inference.kt @@ -145,7 +145,7 @@ class Inference internal constructor(val start: Node, override val ctx: Translat it, ) - // Add it to the scope + // Add it to the scope (and AST) scopeManager.addDeclaration(inferred) start.addDeclaration(inferred) @@ -154,13 +154,6 @@ class Inference internal constructor(val start: Node, override val ctx: Translat record.staticImports.add(inferred) } - // Some more magic, that adds it to the AST. Note: this might not be 100 % compliant - // with the language, since in some languages the AST of a method declaration could - // be outside a method, but this will do for now - if (record != null && inferred is MethodDeclaration) { - record.addMethod(inferred) - } - // "upgrade" our struct to a class, if it was inferred by us, since we are calling // methods on it. But only if the language supports classes in the first place. if ( @@ -187,7 +180,7 @@ class Inference internal constructor(val start: Node, override val ctx: Translat createInferredParameters(inferred, signature) scopeManager.addDeclaration(inferred) - record?.addConstructor(inferred) + record?.constructors += inferred inferred } diff --git a/cpg-core/src/performanceTest/kotlin/de/fraunhofer/aisec/cpg/graph/WalkerTest.kt b/cpg-core/src/performanceTest/kotlin/de/fraunhofer/aisec/cpg/graph/WalkerTest.kt index 9699c13e090..d3a0d377c31 100644 --- a/cpg-core/src/performanceTest/kotlin/de/fraunhofer/aisec/cpg/graph/WalkerTest.kt +++ b/cpg-core/src/performanceTest/kotlin/de/fraunhofer/aisec/cpg/graph/WalkerTest.kt @@ -76,7 +76,7 @@ class WalkerTest : BaseTest() { method.body = comp - record.addMethod(method) + record.methods += method } // And a couple of fields @@ -89,7 +89,7 @@ class WalkerTest : BaseTest() { lit.value = j field.initializer = lit - record.addField(field) + record.fields += field } tu.addDeclaration(record) diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/edges/EdgesTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/edges/EdgesTest.kt index 36e9e0738f2..f5c28af8e07 100644 --- a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/edges/EdgesTest.kt +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/edges/EdgesTest.kt @@ -35,8 +35,8 @@ class EdgesTest { @Test fun testUnwrap() { with(TestLanguageFrontend()) { - var record = newRecordDeclaration("myRecord", kind = "class") - var method = newMethodDeclaration("myFunc") + val record = newRecordDeclaration("myRecord", kind = "class") + val method = newMethodDeclaration("myFunc") record.methods += method assertEquals(1, record.methods.size) diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/processing/VisitorTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/processing/VisitorTest.kt index 79437ea2b4a..1946dc937fe 100644 --- a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/processing/VisitorTest.kt +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/processing/VisitorTest.kt @@ -33,7 +33,6 @@ import de.fraunhofer.aisec.cpg.frontends.TranslationException import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.Node import de.fraunhofer.aisec.cpg.graph.declarations.* -import de.fraunhofer.aisec.cpg.graph.statements import de.fraunhofer.aisec.cpg.graph.statements.ReturnStatement import de.fraunhofer.aisec.cpg.passes.ImportDependencies import de.fraunhofer.aisec.cpg.processing.strategy.Strategy diff --git a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/DeclaratorHandler.kt b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/DeclaratorHandler.kt index f35a02530f9..b3cdfdd83bf 100644 --- a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/DeclaratorHandler.kt +++ b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/DeclaratorHandler.kt @@ -231,16 +231,16 @@ class DeclaratorHandler(lang: CXXLanguageFrontend) : // We need to check if this function is actually part of a named declaration, such as a // record or a namespace, but defined externally. - var parentScope: NameScope? = null + var namedScoped: NameScope? = null - // Check for function definitions that really belong to a named scoped, i.e. if they + // Check for function definitions that really belong to a named scope, i.e., if they // contain a scope operator. This could either be a namespace or a record. val parent = name.parent if (parent != null) { - // In this case, the name contains a qualifier, and we can try to check, if we have a + // In this case, the name contains a qualifier, and we can try to check if we have a // matching name scope for the parent name. We also need to take the current namespace // into account - parentScope = + namedScoped = frontend.scopeManager.lookupScope( parseName( frontend.scopeManager.currentNamespace @@ -249,7 +249,7 @@ class DeclaratorHandler(lang: CXXLanguageFrontend) : ) ) - declaration = createAppropriateFunction(name, parentScope, ctx.parent) + declaration = createAppropriateFunction(name, namedScoped, ctx.parent) } else if (frontend.scopeManager.isInRecord) { // If the current scope is already a record, it's a method declaration = @@ -263,30 +263,14 @@ class DeclaratorHandler(lang: CXXLanguageFrontend) : val outsideOfScope = frontend.scopeManager.currentScope != declaration.scope // If we know our parent scope, but are outside the actual scope on the AST, we - // need to temporarily enter the scope. This way, we can do a little trick - // and (manually) add the declaration to the AST element of the current scope - // (probably the global scope), but associate it to the named scope. - if (parentScope != null && outsideOfScope) { - // Bypass the scope manager and manually add it to the AST parent - val scopeParent = frontend.scopeManager.currentScope?.astNode - if (scopeParent != null && scopeParent is DeclarationHolder) { - scopeParent.addDeclaration(declaration) - } - - // Enter the name scope - parentScope.astNode?.let { + // need to temporarily enter the scope. This way, we declare this function in the named + // scope but don't add it to its AST (since its outside). + if (namedScoped != null && outsideOfScope) { + // Enter the named scope + namedScoped.astNode?.let { frontend.scopeManager.enterScope(it) frontend.scopeManager.addDeclaration(declaration) } - - // We also need to by-pass the scope manager for this, because it will - // otherwise add the declaration to the AST element of the named scope (the record - // or namespace declaration); in the case of a record declaration to the `methods` - // fields. However, since `methods` is an AST field, (for now) we only want those - // methods in there, that were actual AST - // parents. This is also something that we need to figure out how we want to handle - // this. - parentScope.addSymbol(declaration.symbol, declaration) } // Enter the scope of the function itself @@ -336,8 +320,8 @@ class DeclaratorHandler(lang: CXXLanguageFrontend) : } // Check for varargs. Note the difference to Java: here, we don't have a named array - // containing the varargs, but they are rather treated as kind of an invisible arg list that - // is appended to the original ones. For coherent graph behaviour, we introduce an implicit + // containing the varargs, but they are rather treated as a kind of invisible arg list that + // is appended to the original ones. For coherent graph behavior, we introduce an implicit // declaration that wraps this list if (ctx.takesVarArgs()) { val varargs = newParameterDeclaration("va_args", unknownType(), true) @@ -348,10 +332,10 @@ class DeclaratorHandler(lang: CXXLanguageFrontend) : } frontend.scopeManager.leaveScope(declaration) - // if we know our record declaration, but are outside the actual record, we - // need to leave the record scope again afterwards - if (parentScope != null && outsideOfScope) { - parentScope.astNode?.let { frontend.scopeManager.leaveScope(it) } + // If we know our record declaration, but are outside the actual record, we + // need to leave the record scope again afterward + if (namedScoped != null && outsideOfScope) { + namedScoped.astNode?.let { frontend.scopeManager.leaveScope(it) } } // We recognize an ambiguity here, but cannot solve it at the moment diff --git a/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationHandler.kt b/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationHandler.kt index 4e28d7da0d3..08286f4a3a7 100644 --- a/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationHandler.kt +++ b/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationHandler.kt @@ -78,17 +78,12 @@ class DeclarationHandler(frontend: GoLanguageFrontend) : val record = frontend.scopeManager.lookupScope(fqnRecord)?.astNode as? RecordDeclaration - // now this gets a little bit hacky, we will add it to the record declaration - // this is strictly speaking not 100 % true, since the method property edge is - // marked as AST and in Go a method is not part of the struct's AST but is - // declared outside. In the future, we need to differentiate between just the - // associated members of the class and the pure AST nodes declared in the - // struct itself + // Enter scope of record, so we can later resolve this correctly. We do NOT add + // the method to the AST methods property because it is declared outside of the + // record, but we add it to the symbols by declaring it. if (record != null) { method.recordDeclaration = record - record.addMethod(method) - // Enter scope of record frontend.scopeManager.enterScope(record) } } diff --git a/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/SpecificationHandler.kt b/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/SpecificationHandler.kt index 8d6bde5f3ff..e1fbdbb9181 100644 --- a/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/SpecificationHandler.kt +++ b/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/SpecificationHandler.kt @@ -117,8 +117,8 @@ class SpecificationHandler(frontend: GoLanguageFrontend) : val type = frontend.typeOf(field.type) // A field can also have no name, which means that it is embedded. In this case, it - // can be accessed by the local name of its type and therefore we name the field - // accordingly. We use the modifiers property to denote that this is an embedded + // can be accessed by the local name of its type, and therefore we name the field + // accordingly. We use the "modifiers" property to denote that this is an embedded // field, so we can easily retrieve them later val (fieldName, modifiers) = if (field.names.isEmpty()) { diff --git a/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/GoExtraPass.kt b/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/GoExtraPass.kt index ddbbe28032d..551e3574b7f 100644 --- a/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/GoExtraPass.kt +++ b/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/GoExtraPass.kt @@ -152,7 +152,7 @@ class GoExtraPass(ctx: TranslationContext) : ComponentPass(ctx) { scopeManager.enterScope(record) // Loop through the embedded struct and add their methods to the record's scope. - for (method in record.embeddedStructs.flatMap { it.methods }) { + for (method in record.embeddedStructs.flatMap { it.toType().methods }) { // Add it to the scope, but do NOT add it to the underlying AST field (methods), // otherwise we would duplicate the method in the AST scopeManager.addDeclaration(method) diff --git a/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationTest.kt b/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationTest.kt index fda923e71e2..8c9c42f51e5 100644 --- a/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationTest.kt +++ b/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationTest.kt @@ -52,8 +52,8 @@ class DeclarationTest { val myStruct = main.records["main.MyStruct"] assertNotNull(myStruct) - // Receiver should be null since its unnamed - val myFunc = myStruct.methods["MyFunc"] + // Receiver should be null because its unnamed + val myFunc = myStruct.toType().methods["MyFunc"] assertNotNull(myFunc) assertNull(myFunc.receiver) } @@ -107,8 +107,14 @@ class DeclarationTest { ) var methods = myStruct.methods + assertEquals( + 0, + methods.size, + "Expected no inner methods in struct MyStruct, since Go declares methods outside of the type", + ) - var myFunc = methods.firstOrNull() + val typeMethods = myStruct.toType().methods + var myFunc = typeMethods.firstOrNull() assertNotNull(myFunc) assertFullName("p.MyStruct.MyFunc", myFunc) diff --git a/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/GoLanguageFrontendTest.kt b/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/GoLanguageFrontendTest.kt index 4039a302024..35d3efecae9 100644 --- a/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/GoLanguageFrontendTest.kt +++ b/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/GoLanguageFrontendTest.kt @@ -461,7 +461,7 @@ class GoLanguageFrontendTest : BaseTest() { val myStruct = p.records["MyStruct"] assertNotNull(myStruct) - val methods = myStruct.methods + val methods = myStruct.toType().methods val myFunc = methods.firstOrNull() assertNotNull(myFunc) assertLocalName("MyFunc", myFunc) @@ -962,7 +962,7 @@ class GoLanguageFrontendTest : BaseTest() { assertIs(type) assertLocalName("newType", type) - assertEquals(1, type.recordDeclaration?.methods?.size) + assertEquals(1, type.recordDeclaration?.toType()?.methods?.size) } @Test @@ -1151,7 +1151,7 @@ class GoLanguageFrontendTest : BaseTest() { val funcy = result.calls["funcy"] assertNotNull(funcy) - funcy.invokeEdges.all { it.dynamicInvoke == true } + funcy.invokeEdges.all { it.dynamicInvoke } // We should be able to resolve the call from our stored "do" function to funcy assertInvokes(funcy, result.functions["do"]) diff --git a/cpg-language-java/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/JavaImportResolver.kt b/cpg-language-java/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/JavaImportResolver.kt index 718a630a905..14234e9c5e2 100644 --- a/cpg-language-java/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/JavaImportResolver.kt +++ b/cpg-language-java/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/JavaImportResolver.kt @@ -155,8 +155,8 @@ open class JavaImportResolver(ctx: TranslationContext) : ComponentPass(ctx) { targetMethod.language = base.language targetMethod.isInferred = true - base.addField(targetField) - base.addMethod(targetMethod) + base.fields += targetField + base.methods += targetMethod result.add(targetField) result.add(targetMethod) } From 1630de26f621095fc0a9f1551134a7dac5d541af Mon Sep 17 00:00:00 2001 From: Maximilian Kaul Date: Thu, 9 Oct 2025 09:27:51 +0200 Subject: [PATCH 19/93] Make MCP configurable (#2444) * Make MCP configurable * Readme++ --- README.md | 4 ++++ build.gradle.kts | 6 ++++++ configure_frontends.sh | 2 ++ gradle.properties.example | 3 ++- settings.gradle.kts | 7 ++++++- 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41a4f65fa97..79a89a88bef 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,10 @@ Through the `JepSingleton`, the CPG library will look for well known paths on Li For parsing TypeScript, the necessary TypeScript-based code can be found in the `src/main/nodejs` directory of the `cpg-language-typescript` submodule. Gradle should build the script automatically. The bundles script will be placed inside the jar's resources and should work out of the box. +#### MCP + +[Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro) functionality is provided via the optional `cpg-mcp` module. It can be enabled/disabled via the `gradle.properties` setting `enableMCPModule`. + ### Code Style We use [Google Java Style](https://github.com/google/google-java-format) as a formatting. Please install the appropriate plugin for your IDE, such as the [google-java-format IntelliJ plugin](https://plugins.jetbrains.com/plugin/8527-google-java-format) or [google-java-format Eclipse plugin](https://github.com/google/google-java-format/releases/download/google-java-format-1.6/google-java-format-eclipse-plugin_1.6.0.jar). diff --git a/build.gradle.kts b/build.gradle.kts index f41e5da1fe6..d0725f9d1b8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -128,3 +128,9 @@ val enableINIFrontend: Boolean by extra { enableINIFrontend.toBoolean() } project.logger.lifecycle("INI frontend is ${if (enableINIFrontend) "enabled" else "disabled"}") + +val enableMCPModule: Boolean by extra { + val enableMCPModule: String? by project + enableMCPModule.toBoolean() +} +project.logger.lifecycle("MCP module is ${if (enableMCPModule) "enabled" else "disabled"}") diff --git a/configure_frontends.sh b/configure_frontends.sh index 3fd8e946a7b..8ff8627683e 100755 --- a/configure_frontends.sh +++ b/configure_frontends.sh @@ -62,3 +62,5 @@ answerJVM=$(ask "Do you want to enable the JVM frontend? (currently $(getPropert setProperty "enableJVMFrontend" $answerJVM answerINI=$(ask "Do you want to enable the INI frontend? (currently $(getProperty "enableINIFrontend"))") setProperty "enableINIFrontend" $answerINI +answerMCP=$(ask "Do you want to enable the MCP module? (currently $(getProperty "enableMCPModule"))") +setProperty "enableMCPModule" $answerMCP diff --git a/gradle.properties.example b/gradle.properties.example index 96c60ee7c16..9250e5fe481 100644 --- a/gradle.properties.example +++ b/gradle.properties.example @@ -10,7 +10,8 @@ enableTypeScriptFrontend=true enableRubyFrontend=true enableJVMFrontend=true enableINIFrontend=true +enableMCPModule=true org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled ## Suppress warnings about using the experimental Dokka plugin -org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true \ No newline at end of file +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true diff --git a/settings.gradle.kts b/settings.gradle.kts index 240fa694d62..12a9b87c9f5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,7 +9,6 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":cpg-core") include(":cpg-analysis") include(":cpg-neo4j") -include(":cpg-mcp") include(":cpg-concepts") include(":codyze") @@ -54,6 +53,10 @@ val enableINIFrontend: Boolean by extra { val enableINIFrontend: String? by settings enableINIFrontend.toBoolean() } +val enableMCPModule: Boolean by extra { + val enableMCPModule: String? by settings + enableMCPModule.toBoolean() +} if (enableJavaFrontend) include(":cpg-language-java") if (enableCXXFrontend) include(":cpg-language-cxx") @@ -64,6 +67,8 @@ if (enableTypeScriptFrontend) include(":cpg-language-typescript") if (enableRubyFrontend) include(":cpg-language-ruby") if (enableJVMFrontend) include(":cpg-language-jvm") if (enableINIFrontend) include(":cpg-language-ini") +if (enableMCPModule) include(":cpg-mcp") + kover { enableCoverage() From 3e4d65c9222123652b50f8eb10fa58f9d0addd90 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Thu, 9 Oct 2025 09:51:21 +0200 Subject: [PATCH 20/93] Changing function signature of `parse` to accept the file content instead of a file (#1706) * Changing function signature of `parse` to accept the file content instead of a file This PR changes the way `parse` works (in a backwards compatible way). Instead of parsing a `File`, we parse the file contents (and a path). The reasoning behind this is that almost all language frontends currently need to read the file contents and we can harmonize this. This will also allow us to provide more common statistics about the parsing context in the future. * Fixed spotless * fixing uri handling due to it being nullable now * Fixing parameter order * Addign a Converter for the newly introduced TranslationStats --------- Co-authored-by: Konrad Weiss --- .../de/fraunhofer/aisec/codyze/Sarif.kt | 6 +- .../aisec/cpg/TranslationManager.kt | 55 +++++++++++++++- .../fraunhofer/aisec/cpg/TranslationResult.kt | 4 ++ .../fraunhofer/aisec/cpg/TranslationStats.kt | 39 ++++++++++++ .../aisec/cpg/frontends/SupportsNewParse.kt | 38 +++++++++++ .../aisec/cpg/helpers/MeasurementHolder.kt | 8 ++- .../neo4j/TranslationStatsConverter.kt | 63 +++++++++++++++++++ .../aisec/cpg/sarif/PhysicalLocation.kt | 19 +++--- .../frontends/ruby/RubyLanguageFrontend.kt | 16 ++++- 9 files changed, 229 insertions(+), 19 deletions(-) create mode 100644 cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationStats.kt create mode 100644 cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/SupportsNewParse.kt create mode 100644 cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/neo4j/TranslationStatsConverter.kt diff --git a/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/Sarif.kt b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/Sarif.kt index 8bebfe17a49..d5434d9500a 100644 --- a/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/Sarif.kt +++ b/codyze-core/src/main/kotlin/de/fraunhofer/aisec/codyze/Sarif.kt @@ -249,15 +249,15 @@ fun Map.Entry.toSarifLocation( fun de.fraunhofer.aisec.cpg.sarif.PhysicalLocation.toSarif( component: Component? = null ): PhysicalLocation { - var uri = this.artifactLocation.uri.toPath() + var uri = this.artifactLocation.uri?.toPath() val uriBase = component?.location?.artifactLocation?.uri?.toPath() - if (uriBase != null) { + if (uri != null && uriBase != null) { uri = uri.relativeToOrSelf(uriBase) } return PhysicalLocation( artifactLocation = - ArtifactLocation(uri = uri.toString(), uriBaseID = component?.name?.localName), + ArtifactLocation(uri = uri?.toString(), uriBaseID = component?.name?.localName), region = Region( startLine = this.region.startLine.toLong(), diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationManager.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationManager.kt index b48c0772054..4aca17c4bbb 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationManager.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationManager.kt @@ -26,6 +26,10 @@ package de.fraunhofer.aisec.cpg import de.fraunhofer.aisec.cpg.frontends.* +import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend +import de.fraunhofer.aisec.cpg.frontends.SupportsNewParse +import de.fraunhofer.aisec.cpg.frontends.SupportsParallelParsing +import de.fraunhofer.aisec.cpg.frontends.TranslationException import de.fraunhofer.aisec.cpg.graph.Component import de.fraunhofer.aisec.cpg.graph.Name import de.fraunhofer.aisec.cpg.graph.scopes.GlobalScope @@ -43,8 +47,11 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionException import java.util.concurrent.ExecutionException import java.util.concurrent.atomic.AtomicBoolean +import kotlin.io.path.absolute import kotlin.io.path.name +import kotlin.io.path.readText import kotlin.reflect.full.findAnnotation +import kotlin.time.DurationUnit import org.slf4j.LoggerFactory /** Main entry point for all source code translation for all language front-ends. */ @@ -111,6 +118,15 @@ private constructor( } } + log.info( + "Translated {} LoC in total ({} / LoC)", + result.stats.totalLinesOfCode, + (outerBench.duration / result.stats.totalLinesOfCode).toString( + DurationUnit.MILLISECONDS, + decimals = 3, + ), + ) + return result } @@ -375,7 +391,7 @@ private constructor( val future = CompletableFuture.supplyAsync { try { - return@supplyAsync parse(component, ctx, globalCtx, sourceLocation) + return@supplyAsync parse(component, result, ctx, globalCtx, sourceLocation) } catch (e: TranslationException) { throw RuntimeException("Error parsing $sourceLocation", e) } @@ -431,7 +447,7 @@ private constructor( for (sourceLocation in sourceLocations) { ctx.currentComponent = component - val f = parse(component, ctx, ctx, sourceLocation) + val f = parse(component, result, ctx, ctx, sourceLocation) if (f != null) { handleCompletion(result, usedFrontends, sourceLocation, f) } @@ -459,6 +475,7 @@ private constructor( @Throws(TranslationException::class) private fun parse( component: Component, + result: TranslationResult, ctx: TranslationContext, globalCtx: TranslationContext, sourceLocation: File, @@ -479,7 +496,30 @@ private constructor( } return null } - component.addTranslationUnit(frontend.parse(sourceLocation)) + + // Check, if the frontend supports the new API + var tu = + if (frontend is SupportsNewParse) { + // Read the file contents and supply it to the frontend. This gives us a chance + // to do some statistics here, for example on the lines of code. For now, we + // just print it, in a future PR we will gather this information and consolidate + // it. + var path = sourceLocation.toPath().absolute() + var content = path.readText() + var linesOfCode = content.linesOfCode + + log.info("{} has {} LoC", path, linesOfCode) + + var tu = frontend.parse(content, path) + + // Add the LoC. This needs to be synchronized on the stats object, because of + // parallel parsing + synchronized(result.stats) { result.stats.totalLinesOfCode += linesOfCode } + tu + } else { + frontend.parse(sourceLocation) + } + component.addTranslationUnit(tu) } catch (ex: TranslationException) { log.error("An error occurred during parsing of ${sourceLocation.name}: ${ex.message}") if (config.failOnError) { @@ -580,3 +620,12 @@ private fun MutableList.updateGlobalScope(newGlobalScope: GlobalScope?) { type.secondOrderTypes.updateGlobalScope(newGlobalScope) } } + +/** + * This returns a VERY trivial count of the lines of code (mainly just the line count). This can be + * extended to a real LoC algorithm at some point. + */ +val String.linesOfCode: Int + get() { + return this.count { it == '\n' } + } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationResult.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationResult.kt index 39c54f3f7db..782986db40d 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationResult.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationResult.kt @@ -37,6 +37,7 @@ import de.fraunhofer.aisec.cpg.graph.edges.ast.astEdgesOf import de.fraunhofer.aisec.cpg.graph.edges.unwrapping import de.fraunhofer.aisec.cpg.helpers.MeasurementHolder import de.fraunhofer.aisec.cpg.helpers.StatisticsHolder +import de.fraunhofer.aisec.cpg.helpers.neo4j.TranslationStatsConverter import de.fraunhofer.aisec.cpg.passes.ImportDependencies import de.fraunhofer.aisec.cpg.passes.ImportResolver import de.fraunhofer.aisec.cpg.passes.Pass @@ -48,6 +49,7 @@ import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KClass import org.neo4j.ogm.annotation.Relationship import org.neo4j.ogm.annotation.Transient +import org.neo4j.ogm.annotation.typeconversion.Convert /** * The global (intermediate) result of the translation. A [LanguageFrontend] will initially populate @@ -101,6 +103,8 @@ class TranslationResult( val isCancelled: Boolean get() = translationManager.isCancelled() + @Convert(TranslationStatsConverter::class) var stats = TranslationStats() + /** * Checks if only a single software component has been analyzed and returns its translation * units. For multiple software components, it aggregates the results. diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationStats.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationStats.kt new file mode 100644 index 00000000000..a97cf11be7a --- /dev/null +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationStats.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024, 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.cpg + +import de.fraunhofer.aisec.cpg.helpers.MeasurementHolder +import de.fraunhofer.aisec.cpg.helpers.StatisticsHolder + +/** + * This class provides some statistics about our translation process. At some point this will fully + * replace [StatisticsHolder] and [MeasurementHolder] + */ +class TranslationStats { + + /** The total lines of code that were translated into the CPG. */ + var totalLinesOfCode: Int = 0 +} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/SupportsNewParse.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/SupportsNewParse.kt new file mode 100644 index 00000000000..7bb3538062e --- /dev/null +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/SupportsNewParse.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024, 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.cpg.frontends + +import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration +import java.nio.file.Path + +interface SupportsNewParse { + /** + * Parses the given [content] with the language frontend into a [TranslationUnitDeclaration]. If + * known, a [path] should be specified, so that the language frontend can potentially use more + * advanced features like module resolution. + */ + fun parse(content: String, path: Path? = null): TranslationUnitDeclaration +} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/MeasurementHolder.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/MeasurementHolder.kt index 16060dada43..ca53e5dfacd 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/MeasurementHolder.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/MeasurementHolder.kt @@ -35,6 +35,8 @@ import java.nio.file.Path import java.time.Duration import java.time.Instant import java.util.* +import kotlin.time.DurationUnit +import kotlin.time.toDuration import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -179,6 +181,7 @@ constructor( ) : MeasurementHolder(c, message, debug, holder) { private val start: Instant + var duration: kotlin.time.Duration = kotlin.time.Duration.ZERO /** Stops this benchmark and adds its measurement to the its [StatisticsHolder]. */ fun stop() { @@ -187,7 +190,7 @@ constructor( /** Stops the time and computes the difference between */ override fun addMeasurement(measurementKey: String?, measurementValue: String?): Any? { - val duration = Duration.between(start, Instant.now()).toMillis() + var duration = Duration.between(start, Instant.now()).toMillis() measurements["${caller}: $message"] = "$duration ms" logDebugMsg("$caller: $message done in $duration ms") @@ -195,6 +198,9 @@ constructor( // update our holder, if we have any holder?.addBenchmark(this) + // update our internal duration so that others can access it + this.duration = duration.toDuration(DurationUnit.MILLISECONDS) + return duration } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/neo4j/TranslationStatsConverter.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/neo4j/TranslationStatsConverter.kt new file mode 100644 index 00000000000..dc1840998f9 --- /dev/null +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/neo4j/TranslationStatsConverter.kt @@ -0,0 +1,63 @@ +/* + * 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.cpg.helpers.neo4j + +import de.fraunhofer.aisec.cpg.TranslationStats +import de.fraunhofer.aisec.cpg.graph.Name + +/** + * This converter can be used in a Neo4J session to persist the + * [de.fraunhofer.aisec.cpg.TranslationStats] class into its components: + * - currently only totalLinesOfCode + * + * Additionally, it converts the aforementioned Neo4J attributes in a node back into a [Name]. + */ +class TranslationStatsConverter : CpgCompositeConverter { + + companion object { + const val FIELD_TOTAL_LINES_OF_CODE = "totalLinesOfCode" + } + + override fun toGraphProperties(value: TranslationStats?): MutableMap { + val map = mutableMapOf() + + if (value != null) { + + map[FIELD_TOTAL_LINES_OF_CODE] = value.totalLinesOfCode.toString() + } + + return map + } + + override val graphSchema: List> + get() = listOf(Pair("String", FIELD_TOTAL_LINES_OF_CODE)) + + override fun toEntityAttribute(value: MutableMap): TranslationStats { + return TranslationStats().also { + it.totalLinesOfCode = Integer.parseInt(value[FIELD_TOTAL_LINES_OF_CODE] as String) + } + } +} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/sarif/PhysicalLocation.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/sarif/PhysicalLocation.kt index 74e0245f869..40ab4b9b98b 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/sarif/PhysicalLocation.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/sarif/PhysicalLocation.kt @@ -31,14 +31,19 @@ import java.nio.file.Path import java.util.* /** A SARIF compatible location referring to a location, i.e. file and region within the file. */ -class PhysicalLocation(uri: URI, region: Region) { - class ArtifactLocation(val uri: URI) { +class PhysicalLocation(uri: URI?, region: Region) { + class ArtifactLocation(val uri: URI?) { override fun toString(): String { return fileName } - val fileName = uri.path.substring(uri.path.lastIndexOf('/') + 1) + val fileName = + if (uri != null) { + uri.path.substring(uri.path.lastIndexOf('/') + 1) + } else { + "unknown" + } override fun equals(other: Any?): Boolean { if (this === other) return true @@ -49,7 +54,7 @@ class PhysicalLocation(uri: URI, region: Region) { override fun hashCode() = Objects.hashCode(fileName) } - val artifactLocation: ArtifactLocation + var artifactLocation: ArtifactLocation var region: Region init { @@ -72,11 +77,7 @@ class PhysicalLocation(uri: URI, region: Region) { companion object { fun locationLink(location: PhysicalLocation?): String { return if (location != null) { - (location.artifactLocation.uri.path + - ":" + - location.region.startLine + - ":" + - location.region.startColumn) + "${location.artifactLocation}:${location.region.startLine}:${location.region.startColumn}" } else "unknown" } } diff --git a/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontend.kt b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontend.kt index c8c4655f884..1b06ab3d00d 100644 --- a/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontend.kt +++ b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontend.kt @@ -27,11 +27,13 @@ package de.fraunhofer.aisec.cpg.frontends.ruby import de.fraunhofer.aisec.cpg.TranslationContext import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend +import de.fraunhofer.aisec.cpg.frontends.SupportsNewParse import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration import de.fraunhofer.aisec.cpg.graph.types.Type import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation import java.io.File +import java.nio.file.Path import org.jruby.Ruby import org.jruby.ast.BlockNode import org.jruby.ast.MethodDefNode @@ -40,19 +42,27 @@ import org.jruby.parser.Parser import org.jruby.parser.ParserConfiguration class RubyLanguageFrontend(ctx: TranslationContext, language: RubyLanguage) : - LanguageFrontend(ctx, language) { + LanguageFrontend(ctx, language), SupportsNewParse { val declarationHandler: DeclarationHandler = DeclarationHandler(this) val expressionHandler: ExpressionHandler = ExpressionHandler(this) val statementHandler: StatementHandler = StatementHandler(this) override fun parse(file: File): TranslationUnitDeclaration { + return parse(file.readText(Charsets.UTF_8), file.toPath()) + } + + override fun parse(content: String, path: Path?): TranslationUnitDeclaration { val ruby = Ruby.getGlobalRuntime() val parser = Parser(ruby) val node = parser.parse( - file.path, - file.inputStream(), + if (path != null) { + path.toString() + } else { + "unknown" + }, + content.byteInputStream(), null, ParserConfiguration(ruby, 0, false, true, false), ) as RootNode From e5fb3ee4c4643b996127318e551bca6ce323305a Mon Sep 17 00:00:00 2001 From: Florian Wendland Date: Thu, 9 Oct 2025 10:27:02 +0200 Subject: [PATCH 21/93] Transition to integration tests for MCP (#2445) * Move unit tests depending on Python language frontend to integration test * Add missing dependency for Python language frontend in integration tests * Remove test condition for available Python language frontend --------- Co-authored-by: Christian Banse --- cpg-mcp/build.gradle.kts | 9 ++ .../kotlin}/mcp/ApplyConceptsTest.kt | 0 .../kotlin/mcp/CpgAnalyzeToolTest.kt | 96 +++++++++++++++++++ .../kotlin}/mcp/CpgLlmAnalyzeToolTest.kt | 0 .../kotlin}/mcp/ListCommandsTest.kt | 0 .../resources/simple_config.py | 0 .../resources/test_example.py | 0 .../aisec/cpg/mcp/CpgAnalyzeToolTest.kt | 60 ------------ 8 files changed, 105 insertions(+), 60 deletions(-) rename cpg-mcp/src/{test/kotlin/de/fraunhofer/aisec/cpg => integrationTest/kotlin}/mcp/ApplyConceptsTest.kt (100%) create mode 100644 cpg-mcp/src/integrationTest/kotlin/mcp/CpgAnalyzeToolTest.kt rename cpg-mcp/src/{test/kotlin/de/fraunhofer/aisec/cpg => integrationTest/kotlin}/mcp/CpgLlmAnalyzeToolTest.kt (100%) rename cpg-mcp/src/{test/kotlin/de/fraunhofer/aisec/cpg => integrationTest/kotlin}/mcp/ListCommandsTest.kt (100%) rename cpg-mcp/src/{test => integrationTest}/resources/simple_config.py (100%) rename cpg-mcp/src/{test => integrationTest}/resources/test_example.py (100%) diff --git a/cpg-mcp/build.gradle.kts b/cpg-mcp/build.gradle.kts index 2ae2d97a276..6392e38420e 100644 --- a/cpg-mcp/build.gradle.kts +++ b/cpg-mcp/build.gradle.kts @@ -75,6 +75,15 @@ dependencies { annotationProcessor(libs.picocli.codegen) integrationTestImplementation(libs.kotlin.reflect) + integrationTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0") + integrationTestImplementation(libs.mcp) + integrationTestImplementation(libs.ktor.serialization.kotlinx.json) + // 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) } implementation(project(":cpg-concepts")) implementation(project(":cpg-analysis")) diff --git a/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/ApplyConceptsTest.kt b/cpg-mcp/src/integrationTest/kotlin/mcp/ApplyConceptsTest.kt similarity index 100% rename from cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/ApplyConceptsTest.kt rename to cpg-mcp/src/integrationTest/kotlin/mcp/ApplyConceptsTest.kt diff --git a/cpg-mcp/src/integrationTest/kotlin/mcp/CpgAnalyzeToolTest.kt b/cpg-mcp/src/integrationTest/kotlin/mcp/CpgAnalyzeToolTest.kt new file mode 100644 index 00000000000..5303685d638 --- /dev/null +++ b/cpg-mcp/src/integrationTest/kotlin/mcp/CpgAnalyzeToolTest.kt @@ -0,0 +1,96 @@ +/* + * 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.cpg.mcp + +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.addCpgAnalyzeTool +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.globalAnalysisResult +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.runCpgAnalyze +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalysisResult +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalyzePayload +import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +class CpgAnalyzeToolTest { + + private lateinit var server: Server + + @Test + fun cpgAnalyzeToolIntegrationTest() = runTest { + val info = Implementation(name = "test-cpg-server", version = "1.0.0") + val options = + ServerOptions( + capabilities = + ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)) + ) + server = Server(info, options) + + server.addCpgAnalyzeTool() + + val inputSchema = buildJsonObject { + put("content", "def hello():\n print('Hello World')") + put("extension", "py") + } + + val request = CallToolRequest(name = "cpg_analyze", arguments = inputSchema) + + val tool = server.tools["cpg_analyze"] ?: error("Tool not registered") + val result = tool.handler(request) + + assertNotNull(globalAnalysisResult, "Result should be set after tool execution") + + val resultContent = result.content.firstOrNull() + assertIs(resultContent) + val resultText = resultContent.text + assertNotNull(resultText, "Result content should not be null") + + val analysisResult = Json.decodeFromString(resultText) + + assertEquals(2, analysisResult.functions) + assertEquals(1, analysisResult.callExpressions) + assertNotNull(analysisResult.nodes) + } + + @Test + fun cpgAnalyzeToolUnitTest() { + val payload = + CpgAnalyzePayload(content = "def hello():\n print('Hello World')", extension = "py") + val analysisResult = runCpgAnalyze(payload) + assertNotNull(globalAnalysisResult, "Result should be set after tool execution") + + assertEquals(2, analysisResult.functions) + assertEquals(1, analysisResult.callExpressions) + assertNotNull(analysisResult.nodes) + } +} diff --git a/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgLlmAnalyzeToolTest.kt b/cpg-mcp/src/integrationTest/kotlin/mcp/CpgLlmAnalyzeToolTest.kt similarity index 100% rename from cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgLlmAnalyzeToolTest.kt rename to cpg-mcp/src/integrationTest/kotlin/mcp/CpgLlmAnalyzeToolTest.kt diff --git a/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/ListCommandsTest.kt b/cpg-mcp/src/integrationTest/kotlin/mcp/ListCommandsTest.kt similarity index 100% rename from cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/ListCommandsTest.kt rename to cpg-mcp/src/integrationTest/kotlin/mcp/ListCommandsTest.kt diff --git a/cpg-mcp/src/test/resources/simple_config.py b/cpg-mcp/src/integrationTest/resources/simple_config.py similarity index 100% rename from cpg-mcp/src/test/resources/simple_config.py rename to cpg-mcp/src/integrationTest/resources/simple_config.py diff --git a/cpg-mcp/src/test/resources/test_example.py b/cpg-mcp/src/integrationTest/resources/test_example.py similarity index 100% rename from cpg-mcp/src/test/resources/test_example.py rename to cpg-mcp/src/integrationTest/resources/test_example.py diff --git a/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt b/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt index 5b377f3730e..8b91bd8d930 100644 --- a/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt +++ b/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt @@ -26,75 +26,15 @@ package de.fraunhofer.aisec.cpg.mcp import de.fraunhofer.aisec.cpg.mcp.mcpserver.configureServer -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.addCpgAnalyzeTool -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.globalAnalysisResult -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.runCpgAnalyze -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalysisResult -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalyzePayload import io.modelcontextprotocol.kotlin.sdk.* import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertNotNull -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put class CpgAnalyzeToolTest { private lateinit var server: Server - @Test - fun cpgAnalyzeToolIntegrationTest() = runTest { - val info = Implementation(name = "test-cpg-server", version = "1.0.0") - val options = - ServerOptions( - capabilities = - ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)) - ) - server = Server(info, options) - - server.addCpgAnalyzeTool() - - val inputSchema = buildJsonObject { - put("content", "def hello():\n print('Hello World')") - put("extension", "py") - } - - val request = CallToolRequest(name = "cpg_analyze", arguments = inputSchema) - - val tool = server.tools["cpg_analyze"] ?: error("Tool not registered") - val result = tool.handler(request) - - assertNotNull(globalAnalysisResult, "Result should be set after tool execution") - - val resultContent = result.content.firstOrNull() - assertIs(resultContent) - val resultText = resultContent.text - assertNotNull(resultText, "Result content should not be null") - - val analysisResult = Json.decodeFromString(resultText) - - assertEquals(2, analysisResult.functions) - assertEquals(1, analysisResult.callExpressions) - assertNotNull(analysisResult.nodes) - } - - @Test - fun cpgAnalyzeToolUnitTest() { - val payload = - CpgAnalyzePayload(content = "def hello():\n print('Hello World')", extension = "py") - val analysisResult = runCpgAnalyze(payload) - assertNotNull(globalAnalysisResult, "Result should be set after tool execution") - - assertEquals(2, analysisResult.functions) - assertEquals(1, analysisResult.callExpressions) - assertNotNull(analysisResult.nodes) - } - @Test fun testConfigureServer() { val testServer = configureServer() From dd0c0775f63e4522ec719cb785478d4a9478eac8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 08:15:00 +0000 Subject: [PATCH 22/93] Update dependency com.charleskorn.kaml:kaml to v0.98.0 (#2448) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b6de63fccb3..1f455f8d1ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ publish = "0.34.0" sootup = "2.0.0" slf4j = "2.0.16" clikt = "5.0.2" -kaml = "0.97.0" +kaml = "0.98.0" sarif4k = "0.6.0" ktor = "3.3.0" node = "22.14.0" From 00a0f181fb78500f1a3a5557fea345e2733f35b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:17:14 +0200 Subject: [PATCH 23/93] Update dependency @sveltejs/kit to v2.46.4 (#2447) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 36a0fac67a2..8bc67663248 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -23,10 +23,10 @@ importers: version: 5.2.7 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.8(@sveltejs/kit@2.44.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) + version: 3.0.8(@sveltejs/kit@2.46.4(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) '@sveltejs/kit': specifier: ^2.20.6 - version: 2.44.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 2.46.4(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 version: 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) @@ -502,8 +502,8 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.44.0': - resolution: {integrity: sha512-xU5qP7PiYmrSH70Whm/I+nf0j4xBnHyRQNkC1SEfaBOwCCkkeuL6WNxSb8q4Ib7+Z+sZ4JUTDYHfoyVm02EXVQ==} + '@sveltejs/kit@2.46.4': + resolution: {integrity: sha512-J1fd80WokLzIm6EAV7z7C2+/C02qVAX645LZomARARTRJkbbJSY1Jln3wtBZYibUB8c9/5Z6xqLAV39VdbtWCQ==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -1841,11 +1841,11 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.44.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.46.4(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.44.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.46.4(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) - '@sveltejs/kit@2.44.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/kit@2.46.4(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) From 71b7dd6a9bf0cdf832abe798027d7c3b45a4c7c3 Mon Sep 17 00:00:00 2001 From: Konrad Weiss Date: Tue, 14 Oct 2025 09:16:20 +0200 Subject: [PATCH 24/93] Fixing comment matching on multiple nested namespaces with no location (#2446) * Merging namespace skip if it has no location with ast tree exploration if nodes have no location to better match * moving iterative search into getEnclosingChild * Add condidate search to enclosing node search and neares node search in comment matcher * Move back testfile * Some refactoring * Nesting the comment test file into several folders to implicitly create namespaces --- .../aisec/cpg/helpers/CommentMatcher.kt | 51 +++++++++---------- .../frontends/python/PythonFrontendTest.kt | 6 ++- .../innerNamespace}/comments.py | 0 3 files changed, 30 insertions(+), 27 deletions(-) rename cpg-language-python/src/test/resources/python/{ => outerNamespace/innerNamespace}/comments.py (100%) diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/CommentMatcher.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/CommentMatcher.kt index 72053149b2c..3c961538497 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/CommentMatcher.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/CommentMatcher.kt @@ -27,7 +27,6 @@ package de.fraunhofer.aisec.cpg.helpers import de.fraunhofer.aisec.cpg.graph.AstNode import de.fraunhofer.aisec.cpg.graph.Node -import de.fraunhofer.aisec.cpg.graph.declarations.NamespaceDeclaration import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation import de.fraunhofer.aisec.cpg.sarif.Region @@ -54,14 +53,12 @@ class CommentMatcher { .filter { artifactLocation == null || artifactLocation == it.location?.artifactLocation } - .toMutableList() - // As some frontends add regional implicit namespaces we have to search amongst its children - // instead. - children.addAll( - children.filterIsInstance().flatMap { namespace -> - namespace.astChildren.filter { it !in children } - } - ) + .toMutableSet() + + // When a child has no location we can not properly decide if it encloses the comment, we + // instead consider its children with locations. + expandCandidatesByLocation(children) + val enclosing = children.firstOrNull { val nodeRegion: Region = it.location?.region ?: Region() @@ -102,24 +99,9 @@ class CommentMatcher { } .toMutableSet() - // Because we sometimes wrap all elements into a NamespaceDeclaration we have to extract the - // children with a location - children.addAll( - children.filterIsInstance().flatMap { namespace -> - namespace.astChildren.filter { it !in children } - } - ) - // When a child has no location we can not properly consider it for comment matching, - // however, it might have - // a child with a location that we want to consider, this can overlap with namespaces but - // nodes are considered - // only once in the set - children.addAll( - children - .filter { node -> node.location == null || node.location?.region == Region() } - .flatMap { locationLess -> locationLess.astChildren.filter { it !in children } } - ) + // however, instead we consider its contained children that have a location. + expandCandidatesByLocation(children) // Searching for the closest successor to our comment amongst the children of the smallest // enclosing nodes @@ -173,4 +155,21 @@ class CommentMatcher { closest.comment = (closest.comment ?: "") + comment } + + /** + * Expands the given list of candidates by exploring their children iteratively until all + * candidates without a location are expanded by their children with a location. + */ + fun expandCandidatesByLocation(candidates: MutableSet) { + var locationLess = + candidates.filter { node -> node.location == null || node.location?.region == Region() } + while (locationLess.isNotEmpty()) { + val containedChildren = locationLess.flatMap { it.astChildren } + locationLess = + containedChildren + .filter { node -> node.location == null || node.location?.region == Region() } + .filter { it !in candidates } + candidates.addAll(containedChildren) + } + } } diff --git a/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonFrontendTest.kt b/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonFrontendTest.kt index a69c5d9bf27..86a855730e1 100644 --- a/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonFrontendTest.kt +++ b/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonFrontendTest.kt @@ -1098,7 +1098,11 @@ class PythonFrontendTest : BaseTest() { fun testCommentMatching() { val topLevel = Path.of("src", "test", "resources", "python") val tu = - analyzeAndGetFirstTU(listOf(topLevel.resolve("comments.py").toFile()), topLevel, true) { + analyzeAndGetFirstTU( + listOf(topLevel.resolve("outerNamespace").toFile()), + topLevel, + true, + ) { it.registerLanguage().matchCommentsToNodes(true) } assertNotNull(tu) diff --git a/cpg-language-python/src/test/resources/python/comments.py b/cpg-language-python/src/test/resources/python/outerNamespace/innerNamespace/comments.py similarity index 100% rename from cpg-language-python/src/test/resources/python/comments.py rename to cpg-language-python/src/test/resources/python/outerNamespace/innerNamespace/comments.py From 4fcf45385d9512518d3bff8423007079677f64c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:37:12 +0000 Subject: [PATCH 25/93] Update dependency globals to v16.4.0 (#2451) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 8bc67663248..251c738fe7c 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -53,7 +53,7 @@ importers: version: 3.12.4(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4) globals: specifier: ^16.0.0 - version: 16.3.0 + version: 16.4.0 inter-ui: specifier: ^4.1.0 version: 4.1.0 @@ -986,8 +986,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@16.3.0: - resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} engines: {node: '>=18'} graceful-fs@4.2.11: @@ -2211,7 +2211,7 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 eslint: 9.27.0(jiti@2.4.2) esutils: 2.0.3 - globals: 16.3.0 + globals: 16.4.0 known-css-properties: 0.37.0 postcss: 8.5.6 postcss-load-config: 3.1.4(postcss@8.5.6) @@ -2370,7 +2370,7 @@ snapshots: globals@14.0.0: {} - globals@16.3.0: {} + globals@16.4.0: {} graceful-fs@4.2.11: {} From e798ad567ab5140777479c3340fd183f17d42028 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 08:41:13 +0200 Subject: [PATCH 26/93] Update dependency @sveltejs/kit to v2.47.2 (#2450) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 251c738fe7c..aa8d6edba97 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -23,10 +23,10 @@ importers: version: 5.2.7 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.8(@sveltejs/kit@2.46.4(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) + version: 3.0.8(@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) '@sveltejs/kit': specifier: ^2.20.6 - version: 2.46.4(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 version: 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) @@ -502,8 +502,8 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.46.4': - resolution: {integrity: sha512-J1fd80WokLzIm6EAV7z7C2+/C02qVAX645LZomARARTRJkbbJSY1Jln3wtBZYibUB8c9/5Z6xqLAV39VdbtWCQ==} + '@sveltejs/kit@2.47.2': + resolution: {integrity: sha512-mbUomaJTiADTrq6GT4ZvQ7v1rs0S+wXGMzrjFwjARAKMEF8FpOUmz2uEJ4M9WMJMQOXCMHpKFzJfdjo9O7M22A==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -825,8 +825,8 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} - devalue@5.3.2: - resolution: {integrity: sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==} + devalue@5.4.1: + resolution: {integrity: sha512-YtoaOfsqjbZQKGIMRYDWKjUmSB4VJ/RElB+bXZawQAQYAo4xu08GKTMVlsZDTF6R2MbAgjcAQRPI5eIyRAT2OQ==} enhanced-resolve@5.18.1: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} @@ -1841,11 +1841,11 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.46.4(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.46.4(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) - '@sveltejs/kit@2.46.4(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) @@ -1853,7 +1853,7 @@ snapshots: '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.3.2 + devalue: 5.4.1 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.19 @@ -2163,7 +2163,7 @@ snapshots: detect-libc@2.0.4: {} - devalue@5.3.2: {} + devalue@5.4.1: {} enhanced-resolve@5.18.1: dependencies: From bdef6b6f42138449754df715ef5a7b8b12a94717 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:45:51 +0200 Subject: [PATCH 27/93] Update dependency vite to v6.4.1 [SECURITY] (#2453) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 449 +++++++++--------- 1 file changed, 229 insertions(+), 220 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index aa8d6edba97..baa29a2284d 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -23,13 +23,13 @@ importers: version: 5.2.7 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.8(@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))) + version: 3.0.8(@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))) '@sveltejs/kit': specifier: ^2.20.6 - version: 2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 - version: 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) '@tailwindcss/forms': specifier: ^0.5.10 version: 0.5.10(tailwindcss@4.1.7) @@ -38,7 +38,7 @@ importers: version: 0.5.16(tailwindcss@4.1.7) '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.1.7(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + version: 4.1.7(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) cookie: specifier: 1.0.2 version: 1.0.2 @@ -86,7 +86,7 @@ importers: version: 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) vite: specifier: ^6.0.0 - version: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) + version: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) packages: @@ -94,158 +94,158 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@esbuild/aix-ppc64@0.25.9': - resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} + '@esbuild/aix-ppc64@0.25.11': + resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.9': - resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} + '@esbuild/android-arm64@0.25.11': + resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.9': - resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} + '@esbuild/android-arm@0.25.11': + resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.9': - resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} + '@esbuild/android-x64@0.25.11': + resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.9': - resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} + '@esbuild/darwin-arm64@0.25.11': + resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.9': - resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} + '@esbuild/darwin-x64@0.25.11': + resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.9': - resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} + '@esbuild/freebsd-arm64@0.25.11': + resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.9': - resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} + '@esbuild/freebsd-x64@0.25.11': + resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.9': - resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} + '@esbuild/linux-arm64@0.25.11': + resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.9': - resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} + '@esbuild/linux-arm@0.25.11': + resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.9': - resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} + '@esbuild/linux-ia32@0.25.11': + resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.9': - resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} + '@esbuild/linux-loong64@0.25.11': + resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.9': - resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} + '@esbuild/linux-mips64el@0.25.11': + resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.9': - resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} + '@esbuild/linux-ppc64@0.25.11': + resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.9': - resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} + '@esbuild/linux-riscv64@0.25.11': + resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.9': - resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} + '@esbuild/linux-s390x@0.25.11': + resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.9': - resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} + '@esbuild/linux-x64@0.25.11': + resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.9': - resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} + '@esbuild/netbsd-arm64@0.25.11': + resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.9': - resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} + '@esbuild/netbsd-x64@0.25.11': + resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.9': - resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} + '@esbuild/openbsd-arm64@0.25.11': + resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.9': - resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} + '@esbuild/openbsd-x64@0.25.11': + resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.9': - resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} + '@esbuild/openharmony-arm64@0.25.11': + resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.9': - resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} + '@esbuild/sunos-x64@0.25.11': + resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.9': - resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} + '@esbuild/win32-arm64@0.25.11': + resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.9': - resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} + '@esbuild/win32-ia32@0.25.11': + resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.9': - resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} + '@esbuild/win32-x64@0.25.11': + resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -379,108 +379,113 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@rollup/rollup-android-arm-eabi@4.50.1': - resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} + '@rollup/rollup-android-arm-eabi@4.52.5': + resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.50.1': - resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} + '@rollup/rollup-android-arm64@4.52.5': + resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.50.1': - resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} + '@rollup/rollup-darwin-arm64@4.52.5': + resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.50.1': - resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} + '@rollup/rollup-darwin-x64@4.52.5': + resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.50.1': - resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} + '@rollup/rollup-freebsd-arm64@4.52.5': + resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.50.1': - resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} + '@rollup/rollup-freebsd-x64@4.52.5': + resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.50.1': - resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.50.1': - resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.50.1': - resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} + '@rollup/rollup-linux-arm64-gnu@4.52.5': + resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.50.1': - resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} + '@rollup/rollup-linux-arm64-musl@4.52.5': + resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.50.1': - resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} + '@rollup/rollup-linux-loong64-gnu@4.52.5': + resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.50.1': - resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.50.1': - resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.50.1': - resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} + '@rollup/rollup-linux-riscv64-musl@4.52.5': + resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.50.1': - resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} + '@rollup/rollup-linux-s390x-gnu@4.52.5': + resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.50.1': - resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} + '@rollup/rollup-linux-x64-gnu@4.52.5': + resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.50.1': - resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} + '@rollup/rollup-linux-x64-musl@4.52.5': + resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.50.1': - resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} + '@rollup/rollup-openharmony-arm64@4.52.5': + resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.50.1': - resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} + '@rollup/rollup-win32-arm64-msvc@4.52.5': + resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.50.1': - resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} + '@rollup/rollup-win32-ia32-msvc@4.52.5': + resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.50.1': - resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} + '@rollup/rollup-win32-x64-gnu@4.52.5': + resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.5': + resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} cpu: [x64] os: [win32] @@ -832,8 +837,8 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} - esbuild@0.25.9: - resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} + esbuild@0.25.11: + resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} engines: {node: '>=18'} hasBin: true @@ -1374,8 +1379,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.50.1: - resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} + rollup@4.52.5: + resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1493,8 +1498,8 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vite@6.3.6: - resolution: {integrity: sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==} + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -1572,82 +1577,82 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@esbuild/aix-ppc64@0.25.9': + '@esbuild/aix-ppc64@0.25.11': optional: true - '@esbuild/android-arm64@0.25.9': + '@esbuild/android-arm64@0.25.11': optional: true - '@esbuild/android-arm@0.25.9': + '@esbuild/android-arm@0.25.11': optional: true - '@esbuild/android-x64@0.25.9': + '@esbuild/android-x64@0.25.11': optional: true - '@esbuild/darwin-arm64@0.25.9': + '@esbuild/darwin-arm64@0.25.11': optional: true - '@esbuild/darwin-x64@0.25.9': + '@esbuild/darwin-x64@0.25.11': optional: true - '@esbuild/freebsd-arm64@0.25.9': + '@esbuild/freebsd-arm64@0.25.11': optional: true - '@esbuild/freebsd-x64@0.25.9': + '@esbuild/freebsd-x64@0.25.11': optional: true - '@esbuild/linux-arm64@0.25.9': + '@esbuild/linux-arm64@0.25.11': optional: true - '@esbuild/linux-arm@0.25.9': + '@esbuild/linux-arm@0.25.11': optional: true - '@esbuild/linux-ia32@0.25.9': + '@esbuild/linux-ia32@0.25.11': optional: true - '@esbuild/linux-loong64@0.25.9': + '@esbuild/linux-loong64@0.25.11': optional: true - '@esbuild/linux-mips64el@0.25.9': + '@esbuild/linux-mips64el@0.25.11': optional: true - '@esbuild/linux-ppc64@0.25.9': + '@esbuild/linux-ppc64@0.25.11': optional: true - '@esbuild/linux-riscv64@0.25.9': + '@esbuild/linux-riscv64@0.25.11': optional: true - '@esbuild/linux-s390x@0.25.9': + '@esbuild/linux-s390x@0.25.11': optional: true - '@esbuild/linux-x64@0.25.9': + '@esbuild/linux-x64@0.25.11': optional: true - '@esbuild/netbsd-arm64@0.25.9': + '@esbuild/netbsd-arm64@0.25.11': optional: true - '@esbuild/netbsd-x64@0.25.9': + '@esbuild/netbsd-x64@0.25.11': optional: true - '@esbuild/openbsd-arm64@0.25.9': + '@esbuild/openbsd-arm64@0.25.11': optional: true - '@esbuild/openbsd-x64@0.25.9': + '@esbuild/openbsd-x64@0.25.11': optional: true - '@esbuild/openharmony-arm64@0.25.9': + '@esbuild/openharmony-arm64@0.25.11': optional: true - '@esbuild/sunos-x64@0.25.9': + '@esbuild/sunos-x64@0.25.11': optional: true - '@esbuild/win32-arm64@0.25.9': + '@esbuild/win32-arm64@0.25.11': optional: true - '@esbuild/win32-ia32@0.25.9': + '@esbuild/win32-ia32@0.25.11': optional: true - '@esbuild/win32-x64@0.25.9': + '@esbuild/win32-x64@0.25.11': optional: true '@eslint-community/eslint-utils@4.5.1(eslint@9.27.0(jiti@2.4.2))': @@ -1768,67 +1773,70 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@rollup/rollup-android-arm-eabi@4.50.1': + '@rollup/rollup-android-arm-eabi@4.52.5': + optional: true + + '@rollup/rollup-android-arm64@4.52.5': optional: true - '@rollup/rollup-android-arm64@4.50.1': + '@rollup/rollup-darwin-arm64@4.52.5': optional: true - '@rollup/rollup-darwin-arm64@4.50.1': + '@rollup/rollup-darwin-x64@4.52.5': optional: true - '@rollup/rollup-darwin-x64@4.50.1': + '@rollup/rollup-freebsd-arm64@4.52.5': optional: true - '@rollup/rollup-freebsd-arm64@4.50.1': + '@rollup/rollup-freebsd-x64@4.52.5': optional: true - '@rollup/rollup-freebsd-x64@4.50.1': + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + '@rollup/rollup-linux-arm-musleabihf@4.52.5': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.50.1': + '@rollup/rollup-linux-arm64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-arm64-gnu@4.50.1': + '@rollup/rollup-linux-arm64-musl@4.52.5': optional: true - '@rollup/rollup-linux-arm64-musl@4.50.1': + '@rollup/rollup-linux-loong64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + '@rollup/rollup-linux-ppc64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.50.1': + '@rollup/rollup-linux-riscv64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.50.1': + '@rollup/rollup-linux-riscv64-musl@4.52.5': optional: true - '@rollup/rollup-linux-riscv64-musl@4.50.1': + '@rollup/rollup-linux-s390x-gnu@4.52.5': optional: true - '@rollup/rollup-linux-s390x-gnu@4.50.1': + '@rollup/rollup-linux-x64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-x64-gnu@4.50.1': + '@rollup/rollup-linux-x64-musl@4.52.5': optional: true - '@rollup/rollup-linux-x64-musl@4.50.1': + '@rollup/rollup-openharmony-arm64@4.52.5': optional: true - '@rollup/rollup-openharmony-arm64@4.50.1': + '@rollup/rollup-win32-arm64-msvc@4.52.5': optional: true - '@rollup/rollup-win32-arm64-msvc@4.50.1': + '@rollup/rollup-win32-ia32-msvc@4.52.5': optional: true - '@rollup/rollup-win32-ia32-msvc@4.50.1': + '@rollup/rollup-win32-x64-gnu@4.52.5': optional: true - '@rollup/rollup-win32-x64-msvc@4.50.1': + '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true '@standard-schema/spec@1.0.0': {} @@ -1841,15 +1849,15 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) - '@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -1862,27 +1870,27 @@ snapshots: set-cookie-parser: 2.7.1 sirv: 3.0.2 svelte: 5.33.4 - vite: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) + vite: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) debug: 4.4.1 svelte: 5.33.4 - vite: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) + vite: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) debug: 4.4.1 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 svelte: 5.33.4 - vite: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) - vitefu: 1.0.6(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)) + vite: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) + vitefu: 1.0.6(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) transitivePeerDependencies: - supports-color @@ -1963,12 +1971,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.1.7 - '@tailwindcss/vite@4.1.7(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1))': + '@tailwindcss/vite@4.1.7(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@tailwindcss/node': 4.1.7 '@tailwindcss/oxide': 4.1.7 tailwindcss: 4.1.7 - vite: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) + vite: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) '@types/cookie@0.6.0': {} @@ -2170,34 +2178,34 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 - esbuild@0.25.9: + esbuild@0.25.11: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.9 - '@esbuild/android-arm': 0.25.9 - '@esbuild/android-arm64': 0.25.9 - '@esbuild/android-x64': 0.25.9 - '@esbuild/darwin-arm64': 0.25.9 - '@esbuild/darwin-x64': 0.25.9 - '@esbuild/freebsd-arm64': 0.25.9 - '@esbuild/freebsd-x64': 0.25.9 - '@esbuild/linux-arm': 0.25.9 - '@esbuild/linux-arm64': 0.25.9 - '@esbuild/linux-ia32': 0.25.9 - '@esbuild/linux-loong64': 0.25.9 - '@esbuild/linux-mips64el': 0.25.9 - '@esbuild/linux-ppc64': 0.25.9 - '@esbuild/linux-riscv64': 0.25.9 - '@esbuild/linux-s390x': 0.25.9 - '@esbuild/linux-x64': 0.25.9 - '@esbuild/netbsd-arm64': 0.25.9 - '@esbuild/netbsd-x64': 0.25.9 - '@esbuild/openbsd-arm64': 0.25.9 - '@esbuild/openbsd-x64': 0.25.9 - '@esbuild/openharmony-arm64': 0.25.9 - '@esbuild/sunos-x64': 0.25.9 - '@esbuild/win32-arm64': 0.25.9 - '@esbuild/win32-ia32': 0.25.9 - '@esbuild/win32-x64': 0.25.9 + '@esbuild/aix-ppc64': 0.25.11 + '@esbuild/android-arm': 0.25.11 + '@esbuild/android-arm64': 0.25.11 + '@esbuild/android-x64': 0.25.11 + '@esbuild/darwin-arm64': 0.25.11 + '@esbuild/darwin-x64': 0.25.11 + '@esbuild/freebsd-arm64': 0.25.11 + '@esbuild/freebsd-x64': 0.25.11 + '@esbuild/linux-arm': 0.25.11 + '@esbuild/linux-arm64': 0.25.11 + '@esbuild/linux-ia32': 0.25.11 + '@esbuild/linux-loong64': 0.25.11 + '@esbuild/linux-mips64el': 0.25.11 + '@esbuild/linux-ppc64': 0.25.11 + '@esbuild/linux-riscv64': 0.25.11 + '@esbuild/linux-s390x': 0.25.11 + '@esbuild/linux-x64': 0.25.11 + '@esbuild/netbsd-arm64': 0.25.11 + '@esbuild/netbsd-x64': 0.25.11 + '@esbuild/openbsd-arm64': 0.25.11 + '@esbuild/openbsd-x64': 0.25.11 + '@esbuild/openharmony-arm64': 0.25.11 + '@esbuild/sunos-x64': 0.25.11 + '@esbuild/win32-arm64': 0.25.11 + '@esbuild/win32-ia32': 0.25.11 + '@esbuild/win32-x64': 0.25.11 escape-string-regexp@4.0.0: {} @@ -2621,31 +2629,32 @@ snapshots: reusify@1.1.0: {} - rollup@4.50.1: + rollup@4.52.5: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.50.1 - '@rollup/rollup-android-arm64': 4.50.1 - '@rollup/rollup-darwin-arm64': 4.50.1 - '@rollup/rollup-darwin-x64': 4.50.1 - '@rollup/rollup-freebsd-arm64': 4.50.1 - '@rollup/rollup-freebsd-x64': 4.50.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.50.1 - '@rollup/rollup-linux-arm-musleabihf': 4.50.1 - '@rollup/rollup-linux-arm64-gnu': 4.50.1 - '@rollup/rollup-linux-arm64-musl': 4.50.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.50.1 - '@rollup/rollup-linux-ppc64-gnu': 4.50.1 - '@rollup/rollup-linux-riscv64-gnu': 4.50.1 - '@rollup/rollup-linux-riscv64-musl': 4.50.1 - '@rollup/rollup-linux-s390x-gnu': 4.50.1 - '@rollup/rollup-linux-x64-gnu': 4.50.1 - '@rollup/rollup-linux-x64-musl': 4.50.1 - '@rollup/rollup-openharmony-arm64': 4.50.1 - '@rollup/rollup-win32-arm64-msvc': 4.50.1 - '@rollup/rollup-win32-ia32-msvc': 4.50.1 - '@rollup/rollup-win32-x64-msvc': 4.50.1 + '@rollup/rollup-android-arm-eabi': 4.52.5 + '@rollup/rollup-android-arm64': 4.52.5 + '@rollup/rollup-darwin-arm64': 4.52.5 + '@rollup/rollup-darwin-x64': 4.52.5 + '@rollup/rollup-freebsd-arm64': 4.52.5 + '@rollup/rollup-freebsd-x64': 4.52.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 + '@rollup/rollup-linux-arm-musleabihf': 4.52.5 + '@rollup/rollup-linux-arm64-gnu': 4.52.5 + '@rollup/rollup-linux-arm64-musl': 4.52.5 + '@rollup/rollup-linux-loong64-gnu': 4.52.5 + '@rollup/rollup-linux-ppc64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-musl': 4.52.5 + '@rollup/rollup-linux-s390x-gnu': 4.52.5 + '@rollup/rollup-linux-x64-gnu': 4.52.5 + '@rollup/rollup-linux-x64-musl': 4.52.5 + '@rollup/rollup-openharmony-arm64': 4.52.5 + '@rollup/rollup-win32-arm64-msvc': 4.52.5 + '@rollup/rollup-win32-ia32-msvc': 4.52.5 + '@rollup/rollup-win32-x64-gnu': 4.52.5 + '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 run-parallel@1.2.0: @@ -2774,22 +2783,22 @@ snapshots: util-deprecate@1.0.2: {} - vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1): + vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1): dependencies: - esbuild: 0.25.9 + esbuild: 0.25.11 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.50.1 + rollup: 4.52.5 tinyglobby: 0.2.15 optionalDependencies: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.30.1 - vitefu@1.0.6(vite@6.3.6(jiti@2.4.2)(lightningcss@1.30.1)): + vitefu@1.0.6(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)): optionalDependencies: - vite: 6.3.6(jiti@2.4.2)(lightningcss@1.30.1) + vite: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) which@2.0.2: dependencies: From 2ba6f1ca62aa7b5c795788351e3de6491a8bedb8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:06:10 +0100 Subject: [PATCH 28/93] Update dependency com.charleskorn.kaml:kaml to v0.102.0 (#2457) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1f455f8d1ba..222fee3fabf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ publish = "0.34.0" sootup = "2.0.0" slf4j = "2.0.16" clikt = "5.0.2" -kaml = "0.98.0" +kaml = "0.102.0" sarif4k = "0.6.0" ktor = "3.3.0" node = "22.14.0" From bee0018cab172369388e66e86f04e962bebdd372 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 03:50:52 +0000 Subject: [PATCH 29/93] Update dependency @sveltejs/kit to v2.48.1 (#2456) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index baa29a2284d..6117c8ccc9d 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -23,10 +23,10 @@ importers: version: 5.2.7 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.8(@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))) + version: 3.0.8(@sveltejs/kit@2.48.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))) '@sveltejs/kit': specifier: ^2.20.6 - version: 2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) + version: 2.48.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 version: 5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) @@ -507,8 +507,8 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.47.2': - resolution: {integrity: sha512-mbUomaJTiADTrq6GT4ZvQ7v1rs0S+wXGMzrjFwjARAKMEF8FpOUmz2uEJ4M9WMJMQOXCMHpKFzJfdjo9O7M22A==} + '@sveltejs/kit@2.48.1': + resolution: {integrity: sha512-CuwgzfHyc8OGI0HNa7ISQHN8u8XyLGM4jeP8+PYig2B15DD9H39KvwQJiUbGU44VsLx3NfwH4OXavIjvp7/6Ww==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -830,8 +830,8 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} - devalue@5.4.1: - resolution: {integrity: sha512-YtoaOfsqjbZQKGIMRYDWKjUmSB4VJ/RElB+bXZawQAQYAo4xu08GKTMVlsZDTF6R2MbAgjcAQRPI5eIyRAT2OQ==} + devalue@5.4.2: + resolution: {integrity: sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==} enhanced-resolve@5.18.1: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} @@ -1165,8 +1165,8 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} @@ -1396,8 +1396,8 @@ packages: engines: {node: '>=10'} hasBin: true - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -1849,11 +1849,11 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.48.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.48.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) - '@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/kit@2.48.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) @@ -1861,13 +1861,13 @@ snapshots: '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.4.1 + devalue: 5.4.2 esm-env: 1.2.2 kleur: 4.1.5 - magic-string: 0.30.19 + magic-string: 0.30.21 mrmime: 2.0.1 sade: 1.8.1 - set-cookie-parser: 2.7.1 + set-cookie-parser: 2.7.2 sirv: 3.0.2 svelte: 5.33.4 vite: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) @@ -1905,7 +1905,7 @@ snapshots: enhanced-resolve: 5.18.1 jiti: 2.4.2 lightningcss: 1.30.1 - magic-string: 0.30.19 + magic-string: 0.30.21 source-map-js: 1.2.1 tailwindcss: 4.1.7 @@ -2171,7 +2171,7 @@ snapshots: detect-libc@2.0.4: {} - devalue@5.4.1: {} + devalue@5.4.2: {} enhanced-resolve@5.18.1: dependencies: @@ -2503,7 +2503,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.19: + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2667,7 +2667,7 @@ snapshots: semver@7.7.2: {} - set-cookie-parser@2.7.1: {} + set-cookie-parser@2.7.2: {} shebang-command@2.0.0: dependencies: From a4b6bd928dc8a7307efeb3ab7f3e38aad5fc9dfe Mon Sep 17 00:00:00 2001 From: KuechA <31155350+KuechA@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:47:55 +0100 Subject: [PATCH 30/93] Expose host and port config in codyze API (#2458) --- .../kotlin/de/fraunhofer/aisec/codyze/console/Main.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt index c1e17de5518..81e45277bc0 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt @@ -37,13 +37,11 @@ import kotlinx.serialization.json.Json /** * This function starts the embedded server for the web console. It uses the Netty engine and - * listens on localhost at port 8080. The server is configured using the [configureWebconsole] - * function. + * listens on [host] (default: localhost) at [port] (default: 8080). The server is configured using + * the [configureWebconsole] function. */ -fun ConsoleService.startConsole() { - embeddedServer(Netty, host = "localhost", port = 8080) { - configureWebconsole(this@startConsole) - } +fun ConsoleService.startConsole(host: String = "localhost", port: Int = 8080) { + embeddedServer(Netty, host = host, port = port) { configureWebconsole(this@startConsole) } .start(wait = true) } From 3e0601edeb5cced49c9c4c3ff34d5b3b02acbea8 Mon Sep 17 00:00:00 2001 From: KuechA <31155350+KuechA@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:25:14 +0100 Subject: [PATCH 31/93] Metric and evidence ids in query trees (#2440) Metric and evidence ids --- .../de/fraunhofer/aisec/cpg/query/QueryTree.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt index 956521c71a9..e657ba198d3 100644 --- a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt @@ -85,6 +85,12 @@ open class QueryTree( val operator: QueryTreeOperators, val collectCallerInfo: Boolean = true, ) : Comparable>, HasAssumptions { + /** The evidence ID is used to track which evidence this query tree represents. */ + var evidenceId: String? = null + + /** The metric ID is used to track which metric this query tree represents. */ + var metricId: String? = null + /** * Determines if the [QueryTree.value] is acceptable after evaluating the [assumptions] which * affect the result. @@ -139,6 +145,16 @@ open class QueryTree( checkForSuppression() } + fun withMetricId(metricId: String?): QueryTree { + this.metricId = metricId + return this + } + + fun withEvidenceId(evidenceId: String?): QueryTree { + this.evidenceId = evidenceId + return this + } + /** * Calculates the confidence of the [QueryTree] based on the [operator] and the [children] of * the [QueryTree]. From cf68b6c8c537080b8a157f0def0d0e6763231551 Mon Sep 17 00:00:00 2001 From: KuechA <31155350+KuechA@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:49:03 +0100 Subject: [PATCH 32/93] Remove `evidenceID` from `QueryTree` (#2460) Remove evidence ID from QueryTree --- .../kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt index e657ba198d3..3e087b1fc46 100644 --- a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/query/QueryTree.kt @@ -85,9 +85,6 @@ open class QueryTree( val operator: QueryTreeOperators, val collectCallerInfo: Boolean = true, ) : Comparable>, HasAssumptions { - /** The evidence ID is used to track which evidence this query tree represents. */ - var evidenceId: String? = null - /** The metric ID is used to track which metric this query tree represents. */ var metricId: String? = null @@ -145,16 +142,12 @@ open class QueryTree( checkForSuppression() } + /** Sets the [metricId] and returns this [QueryTree]. */ fun withMetricId(metricId: String?): QueryTree { this.metricId = metricId return this } - fun withEvidenceId(evidenceId: String?): QueryTree { - this.evidenceId = evidenceId - return this - } - /** * Calculates the confidence of the [QueryTree] based on the [operator] and the [children] of * the [QueryTree]. From c2d3bb156b3594317d72097b3977b9452d9b43f4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 08:17:55 +0100 Subject: [PATCH 33/93] Update dependency eslint-plugin-svelte to v3.13.0 (#2462) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 6117c8ccc9d..dde4d609967 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -50,7 +50,7 @@ importers: version: 10.1.5(eslint@9.27.0(jiti@2.4.2)) eslint-plugin-svelte: specifier: ^3.0.0 - version: 3.12.4(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4) + version: 3.13.0(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4) globals: specifier: ^16.0.0 version: 16.4.0 @@ -256,12 +256,6 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -852,8 +846,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-svelte@3.12.4: - resolution: {integrity: sha512-hD7wPe+vrPgx3U2X2b/wyTMtWobm660PygMGKrWWYTc9lvtY8DpNFDaU2CJQn1szLjGbn/aJ3g8WiXuKakrEkw==} + eslint-plugin-svelte@3.13.0: + resolution: {integrity: sha512-2ohCCQJJTNbIpQCSDSTWj+FN0OVfPmSO03lmSNT7ytqMaWF6kpT86LdzDqtm4sh7TVPl/OEWJ/d7R87bXP2Vjg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 @@ -1391,8 +1385,8 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true @@ -1431,9 +1425,9 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte-eslint-parser@1.3.3: - resolution: {integrity: sha512-oTrDR8Z7Wnguut7QH3YKh7JR19xv1seB/bz4dxU5J/86eJtZOU4eh0/jZq4dy6tAlz/KROxnkRQspv5ZEt7t+Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + svelte-eslint-parser@1.4.0: + resolution: {integrity: sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.18.3} peerDependencies: svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: @@ -1660,11 +1654,6 @@ snapshots: eslint: 9.27.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.7.0(eslint@9.27.0(jiti@2.4.2))': - dependencies: - eslint: 9.27.0(jiti@2.4.2) - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.0(eslint@9.27.0(jiti@2.4.2))': dependencies: eslint: 9.27.0(jiti@2.4.2) @@ -2056,7 +2045,7 @@ snapshots: fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.2 + semver: 7.7.3 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -2064,7 +2053,7 @@ snapshots: '@typescript-eslint/utils@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.27.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.27.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.33.0 '@typescript-eslint/types': 8.33.0 '@typescript-eslint/typescript-estree': 8.33.0(typescript@5.8.3) @@ -2213,7 +2202,7 @@ snapshots: dependencies: eslint: 9.27.0(jiti@2.4.2) - eslint-plugin-svelte@3.12.4(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4): + eslint-plugin-svelte@3.13.0(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.27.0(jiti@2.4.2)) '@jridgewell/sourcemap-codec': 1.5.5 @@ -2224,8 +2213,8 @@ snapshots: postcss: 8.5.6 postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) - semver: 7.7.2 - svelte-eslint-parser: 1.3.3(svelte@5.33.4) + semver: 7.7.3 + svelte-eslint-parser: 1.4.0(svelte@5.33.4) optionalDependencies: svelte: 5.33.4 transitivePeerDependencies: @@ -2665,7 +2654,7 @@ snapshots: dependencies: mri: 1.2.0 - semver@7.7.2: {} + semver@7.7.3: {} set-cookie-parser@2.7.2: {} @@ -2701,7 +2690,7 @@ snapshots: transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.3.3(svelte@5.33.4): + svelte-eslint-parser@1.4.0(svelte@5.33.4): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 From 8f07ef84fc41cf2384843ff1e8331f444ab577af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:02:58 +0100 Subject: [PATCH 34/93] Update dependency black.ninia:jep to v4.3.1 (#2461) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 222fee3fabf..77ae7742677 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,7 +50,7 @@ icu4j = { module = "com.ibm.icu:icu4j", version = "77.1"} eclipse-cdt-core = { module = "11.5/cdt-11.5.0/plugins/org.eclipse.cdt:core", version = "8.4.0.202402110645"} picocli = { module = "info.picocli:picocli", version = "4.7.0"} picocli-codegen = { module = "info.picocli:picocli-codegen", version = "4.7.0"} -jep = { module = "black.ninia:jep", version = "4.2.0" } # build.yml uses grep to extract the jep verison number for CI/CD purposes +jep = { module = "black.ninia:jep", version = "4.3.1" } # build.yml uses grep to extract the jep verison number for CI/CD purposes llvm = { module = "org.bytedeco:llvm-platform", version = "16.0.4-1.5.9"} jruby = { module = "org.jruby:jruby-core", version = "9.4.3.0" } ini4j = { module = "org.ini4j:ini4j", version = "0.5.4" } From f2b79a2a7b7ef428b7cb69b98813b1dc9adf031e Mon Sep 17 00:00:00 2001 From: Florian Wendland Date: Wed, 12 Nov 2025 12:46:09 +0100 Subject: [PATCH 35/93] Propagate information from underlying node to overlay node (#2466) * Propagate information from underlying node to overlay node Set `code` and `location` from `underlyingNode` when setting it * Spotless * Rework information propagation using the onChange callback from `EdgeSingletonList` Pulled callback into `OverlaySingleEdge` to use it. --- .../passes/concepts/OverlayNodeEquality.kt | 2 +- .../fraunhofer/aisec/cpg/graph/OverlayNode.kt | 27 ++++++++++++++++++- .../aisec/cpg/graph/edges/overlay/Overlay.kt | 2 ++ .../aisec/cpg/enhancements/OverlayTest.kt | 20 ++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/cpg-concepts/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/concepts/OverlayNodeEquality.kt b/cpg-concepts/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/concepts/OverlayNodeEquality.kt index dfc24de5dc7..246c069f097 100644 --- a/cpg-concepts/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/concepts/OverlayNodeEquality.kt +++ b/cpg-concepts/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/concepts/OverlayNodeEquality.kt @@ -50,7 +50,7 @@ class OverlayNodeEquality { call2.location = PhysicalLocation(URI("./some/path"), Region(1, 2, 1, 3)) file1.underlyingNode = call1 - assertEquals(file1, file2) + assertNotEquals(file1, file2) file2.underlyingNode = call2 assertNotEquals(file1, file2) } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/OverlayNode.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/OverlayNode.kt index a7e149d76b6..d97c9d3a307 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/OverlayNode.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/OverlayNode.kt @@ -26,6 +26,7 @@ package de.fraunhofer.aisec.cpg.graph import de.fraunhofer.aisec.cpg.frontends.NoLanguage +import de.fraunhofer.aisec.cpg.graph.edges.overlay.OverlayEdge import de.fraunhofer.aisec.cpg.graph.edges.overlay.OverlaySingleEdge import de.fraunhofer.aisec.cpg.graph.edges.unwrapping import java.util.Objects @@ -44,10 +45,34 @@ abstract class OverlayNode() : Node() { @Relationship(value = "OVERLAY", direction = Relationship.Direction.INCOMING) /** All [OverlayNode]s nodes are connected to an original cpg [Node] by this. */ var underlyingNodeEdge: OverlaySingleEdge = - OverlaySingleEdge(this, of = null, mirrorProperty = Node::overlayEdges, outgoing = false) + OverlaySingleEdge( + this, + of = null, + mirrorProperty = Node::overlayEdges, + outgoing = false, + onChange = this::propagateNodePropertiesFromUnderlyingNode, + ) var underlyingNode by unwrapping(OverlayNode::underlyingNodeEdge) + /** + * Propagates information from the [underlyingNode] into this [OverlayNode], when setting an + * [underlyingNode] as part of an [OverlaySingleEdge.onChanged] callback in our delegated + * property [underlyingNodeEdge]. + * + * Note: The new [underlyingNode] is the start node of [newEdge], because our + * [underlyingNodeEdge] is not outgoing. + */ + private fun propagateNodePropertiesFromUnderlyingNode( + oldEdge: OverlayEdge?, + newEdge: OverlayEdge?, + ) { + val newUnderlyingNode = newEdge?.start + + code = newUnderlyingNode?.code + location = newUnderlyingNode?.location + } + /** * Compares this [OverlayNode] to another object. We also include the [underlyingNode] in this * process, meaning that two overlay nodes with the equal properties will not be equal if they diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/edges/overlay/Overlay.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/edges/overlay/Overlay.kt index 560a1f661bb..f5617636211 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/edges/overlay/Overlay.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/edges/overlay/Overlay.kt @@ -59,12 +59,14 @@ class OverlaySingleEdge( of: Node?, override var mirrorProperty: KProperty>, outgoing: Boolean = true, + onChange: ((old: OverlayEdge?, new: OverlayEdge?) -> Unit)? = null, ) : EdgeSingletonList( thisRef = thisRef, init = ::OverlayEdge, outgoing = outgoing, of = of, + onChanged = onChange, ), MirroredEdgeCollection diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/OverlayTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/OverlayTest.kt index 24c4ff21549..ec7802a4ff9 100644 --- a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/OverlayTest.kt +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/OverlayTest.kt @@ -29,6 +29,9 @@ import de.fraunhofer.aisec.cpg.frontends.TestLanguageFrontend import de.fraunhofer.aisec.cpg.graph.Node import de.fraunhofer.aisec.cpg.graph.OverlayNode import de.fraunhofer.aisec.cpg.graph.newLiteral +import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation +import de.fraunhofer.aisec.cpg.sarif.Region +import java.net.URI import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -84,4 +87,21 @@ class OverlayTest { assertTrue(codeNode2.nextDFGEdges.first().overlaying) } } + + @Test + fun testUnderlyingNodePropagation() { + with(TestLanguageFrontend()) { + val overlay: OverlayNode = object : OverlayNode() {} + assertTrue { overlay.underlyingNode == null } + + val node = object : Node() {} + node.location = PhysicalLocation(URI.create(""), Region(10, 10, 10, 10)) + node.code = "Test" + + overlay.underlyingNode = node + assertTrue { overlay.underlyingNode == node } + assertTrue { overlay.location == node.location } + assertTrue { overlay.code == node.code } + } + } } From cf5fc5da0558ceffe383aec2d11036962186966b Mon Sep 17 00:00:00 2001 From: Maximilian Kaul Date: Thu, 13 Nov 2025 11:50:31 +0100 Subject: [PATCH 36/93] Python: remove unsupported versions/add current & future versions (#2467) --- .../de/fraunhofer/aisec/cpg/frontends/python/JepSingleton.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/JepSingleton.kt b/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/JepSingleton.kt index 4495d208aa9..c2f4b38140a 100644 --- a/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/JepSingleton.kt +++ b/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/JepSingleton.kt @@ -64,7 +64,7 @@ object JepSingleton { val virtualEnvName = System.getenv("CPG_PYTHON_VIRTUALENV") ?: "cpg" val virtualEnvPath = Paths.get(System.getProperty("user.home"), ".virtualenvs", virtualEnvName) - val pythonVersions = listOf("3.8", "3.9", "3.10", "3.11", "3.12", "3.13") + val pythonVersions = listOf("3.10", "3.11", "3.12", "3.13", "3.14", "3.15") val wellKnownPaths = mutableListOf() pythonVersions.forEach { version -> // Linux From c2abe82016b5058f9e6c982e4cc352df6c936e4c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 07:56:44 +0100 Subject: [PATCH 37/93] Update dependency com.vanniktech:gradle-maven-publish-plugin to v0.35.0 (#2470) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77ae7742677..9a455373272 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ neo4j = "4.0.10" neo4j5 = "5.28.2" log4j = "2.24.0" spotless = "7.0.4" -publish = "0.34.0" +publish = "0.35.0" sootup = "2.0.0" slf4j = "2.0.16" clikt = "5.0.2" From cc84696d6e779c2f96f8e034a222f601bbcafc2a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:24:34 +0000 Subject: [PATCH 38/93] Update dependency globals to v16.5.0 (#2464) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- codyze-console/src/main/webapp/pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index dde4d609967..ba3ae264b76 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -53,7 +53,7 @@ importers: version: 3.13.0(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4) globals: specifier: ^16.0.0 - version: 16.4.0 + version: 16.5.0 inter-ui: specifier: ^4.1.0 version: 4.1.0 @@ -985,8 +985,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@16.4.0: - resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} graceful-fs@4.2.11: @@ -2208,7 +2208,7 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 eslint: 9.27.0(jiti@2.4.2) esutils: 2.0.3 - globals: 16.4.0 + globals: 16.5.0 known-css-properties: 0.37.0 postcss: 8.5.6 postcss-load-config: 3.1.4(postcss@8.5.6) @@ -2367,7 +2367,7 @@ snapshots: globals@14.0.0: {} - globals@16.4.0: {} + globals@16.5.0: {} graceful-fs@4.2.11: {} From 4b608b9c7762df152487e768449562e37e6ca8c4 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 20 Nov 2025 23:39:36 +0100 Subject: [PATCH 39/93] Use koog for LLM stuff; Fix Chat UI bugs --- codyze-console/build.gradle.kts | 12 +- .../aisec/codyze/console/ChatService.kt | 273 +++++++---------- .../aisec/codyze/console/ConsoleService.kt | 3 + .../fraunhofer/aisec/codyze/console/Main.kt | 21 +- .../fraunhofer/aisec/codyze/console/Router.kt | 16 +- .../src/main/resources/application.conf | 22 +- codyze-console/src/main/webapp/package.json | 3 +- codyze-console/src/main/webapp/pnpm-lock.yaml | 10 + .../ai-assistant/ChatInterface.svelte | 284 +++++++----------- .../ai-assistant/MarkdownRenderer.svelte | 149 +++++++++ .../ai-assistant/MessageInput.svelte | 66 ++++ .../ai-assistant/WelcomeScreen.svelte | 185 +++++------- .../src/lib/components/ai-assistant/index.ts | 3 +- .../lib/components/analysis/CodeViewer.svelte | 2 +- .../lib/components/analysis/FileTree.svelte | 27 +- .../analysis/QueryTreeExplorer.svelte | 7 +- .../webapp/src/lib/services/apiService.ts | 224 +++++++------- .../main/webapp/src/lib/services/llmAgent.ts | 37 ++- .../src/routes/ai-assistant/+page.svelte | 163 ++++++---- .../webapp/src/routes/ai-assistant/+page.ts | 15 +- codyze-console/src/main/webapp/vite.config.ts | 14 +- .../fraunhofer/aisec/cpg/mcp/Application.kt | 6 +- .../cpg/mcp/mcpserver/tools/utils/Util.kt | 35 ++- gradle/libs.versions.toml | 6 + 24 files changed, 891 insertions(+), 692 deletions(-) create mode 100644 codyze-console/src/main/webapp/src/lib/components/ai-assistant/MarkdownRenderer.svelte create mode 100644 codyze-console/src/main/webapp/src/lib/components/ai-assistant/MessageInput.svelte diff --git a/codyze-console/build.gradle.kts b/codyze-console/build.gradle.kts index 6af353b0583..5a8afe42a60 100644 --- a/codyze-console/build.gradle.kts +++ b/codyze-console/build.gradle.kts @@ -28,14 +28,18 @@ dependencies { implementation("io.ktor:ktor-client-cio:${libs.versions.ktor.get()}") implementation("io.ktor:ktor-client-content-negotiation:${libs.versions.ktor.get()}") + // Ktor SSE plugin + implementation("io.ktor:ktor-server-sse:${libs.versions.ktor.get()}") + + implementation(libs.koog.agents) + implementation(libs.koog.tools) + implementation(libs.koog.executor.ollama.client) + implementation(libs.koog.executor.google.client) + // Serialization implementation(libs.kotlinx.serialization.json) implementation(libs.jacksonyml) - implementation("dev.langchain4j:langchain4j-mcp:1.4.0-beta10") - implementation("dev.langchain4j:langchain4j:1.4.0") - implementation("dev.langchain4j:langchain4j-ollama:1.4.0") - // Testing testImplementation(libs.ktor.server.test.host) testImplementation(libs.ktor.client.content.negotiation) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt index 88137ae0601..d3ff95491f3 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt @@ -25,190 +25,129 @@ */ package de.fraunhofer.aisec.codyze.console +import ai.koog.agents.core.agent.AIAgent +import ai.koog.agents.mcp.McpToolRegistryProvider +import ai.koog.prompt.executor.llms.all.simpleGoogleAIExecutor +import ai.koog.prompt.executor.llms.all.simpleOllamaAIExecutor +import ai.koog.prompt.llm.LLMCapability +import ai.koog.prompt.llm.LLMProvider +import ai.koog.prompt.llm.LLModel import com.typesafe.config.ConfigFactory -import de.fraunhofer.aisec.cpg.mcp.mcpserver.configureServer -import de.fraunhofer.aisec.cpg.mcp.mcpserver.listTools -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* -import io.ktor.utils.io.* import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.encodeToJsonElement -import kotlinx.serialization.json.jsonObject @Serializable data class ChatMessageJSON(val role: String, val content: String) @Serializable data class ChatRequestJSON(val messages: List) -enum class LLMProvider { - OLLAMA, - OPENAI, - ANTHROPIC, -} - -@Serializable -private data class LLMRequest( - val model: String, - val messages: List, - val maxTokens: Int? = null, - val stream: Boolean = false, - val tools: List? = null, -) - -@Serializable private data class MCPToolCall(val function: MCPToolCallFunction) - -@Serializable private data class MCPToolCallFunction(val name: String, val arguments: JsonObject) - -@Serializable -private data class LLMResponse( - val model: String? = null, - val message: LLMMessage? = null, - val done: Boolean? = null, -) - -@Serializable -private data class LLMMessage( - val role: String? = null, - val content: String? = null, - val tool_calls: List? = null, -) - -class ChatService { - private val httpClient = - HttpClient(CIO) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } } +class ChatService() { val config = ConfigFactory.load() + val provider: String = config.getString("llm.provider") + val mcpServerUrl: String = config.getString("mcp.serverUrl") + + fun chat(request: ChatRequestJSON): Flow = channelFlow { + val userMessage = request.messages.lastOrNull()?.content ?: "" + + // Connect to MCP server via SSE + val transport = McpToolRegistryProvider.defaultSseTransport(mcpServerUrl) + + // Create tool registry from MCP server + val toolRegistry = + McpToolRegistryProvider.fromTransport( + transport = transport, + name = "codyze-console", + version = "1.0.0", + ) + + // Get provider-specific configuration + val (executor, llmModel) = + when (provider.lowercase()) { + "gemini" -> { + val apiKey = + System.getenv("GEMINI_API_KEY") + ?: throw IllegalStateException( + "GEMINI_API_KEY environment variable not set" + ) + val model = config.getString("llm.gemini.model") + + val executor = simpleGoogleAIExecutor(apiKey = apiKey) + val llmModel = + LLModel( + provider = LLMProvider.Google, + id = model, + capabilities = listOf(LLMCapability.Temperature, LLMCapability.Tools), + contextLength = 128000, + maxOutputTokens = 8192, + ) - val provider = LLMProvider.valueOf(config.getString("llm.provider").uppercase()) - val baseUrl = config.getString("llm.baseUrl") - val model = config.getString("llm.model") - - // MCP Server Integration - private val mcpServer = configureServer() - - /** Get available tools from MCP server (tools/list) */ - private fun getAvailableTools(): List { - return mcpServer.listTools().map { registeredTool -> - Json.encodeToJsonElement(registeredTool).jsonObject - } - } + executor to llmModel + } - /** Execute MCP tool call (tools/call) */ - private fun executeToolCall(toolCall: MCPToolCall): String { - return try { - val toolName = toolCall.function.name - val mcpTool = mcpServer.tools[toolName] - - if (mcpTool != null) { - // TODO: Invoke tool call properly - // val result = mcpTool.handler.invoke(toolCall.function.arguments) - // For now, simulate execution - "Tool '$toolName' executed with args: ${toolCall.function.arguments}" - } else { - "Error: Tool '$toolName' not found" - } - } catch (e: Exception) { - "Error executing tool '${toolCall.function.name}': ${e.message}" - } - } + "ollama" -> { + val baseUrl = config.getString("llm.ollama.baseUrl") + val model = config.getString("llm.ollama.model") + + val executor = simpleOllamaAIExecutor(baseUrl = baseUrl) + val llmModel = + LLModel( + provider = LLMProvider.Ollama, + id = model, + capabilities = listOf(LLMCapability.Tools), + contextLength = 65536, + maxOutputTokens = 8192, + ) - fun chat(request: ChatRequestJSON): Flow = flow { - if (baseUrl.isNullOrEmpty()) { - emit("{\"error\":\"LLM configuration missing: LLM_BASE_URL not set\"}") - return@flow - } - - if (model.isNullOrEmpty()) { - emit("{\"error\":\"LLM configuration missing: LLM_MODEL not set\"}") - return@flow - } - - try { - when (provider) { - LLMProvider.OLLAMA -> { - chatOllamaStream(request.messages).collect { chunk -> emit(chunk) } + executor to llmModel } - LLMProvider.OPENAI -> TODO() - LLMProvider.ANTHROPIC -> TODO() - } - } catch (e: Exception) { - emit("{\"error\":\"Chat error: ${e.message}\"}") - } - } - - private fun chatOllamaStream(messages: List): Flow = flow { - try { - // Get available MCP tools - val availableTools = getAvailableTools() - - val response = - httpClient.post("$baseUrl/api/chat") { - contentType(ContentType.Application.Json) - setBody( - LLMRequest( - model = model!!, - messages = messages, - stream = true, - tools = availableTools.ifEmpty { null }, + "vLLM" -> { + val baseUrl = config.getString("llm.vLLM.baseUrl") + val model = config.getString("llm.vLLM.model") + + val executor = simpleOllamaAIExecutor(baseUrl = baseUrl) + val llmModel = + LLModel( + provider = LLMProvider.OpenAI, + id = model, + capabilities = listOf(LLMCapability.Tools), + contextLength = 65536, + maxOutputTokens = 8192, ) - ) - } - if (response.status.isSuccess()) { - val channel = response.bodyAsChannel() - val jsonParser = Json { ignoreUnknownKeys = true } - - while (!channel.isClosedForRead) { - val chunk = channel.readUTF8Line() - if (chunk != null && chunk.isNotBlank()) { - try { - val llmResponse = jsonParser.decodeFromString(chunk) - - if (llmResponse.done == true) { - break - } - - val message = llmResponse.message - - // Handle text content - val content = message?.content - if (!content.isNullOrEmpty()) { - emit(content) - } - - // Handle tool calls - val toolCalls = message?.tool_calls - if (!toolCalls.isNullOrEmpty()) { - emit("\n\n**Tools Called:**\n") - for (toolCall in toolCalls) { - emit("- **${toolCall.function.name}**\n") - val result = executeToolCall(toolCall) - emit(" Result: $result\n") - } - - emit("\n") - } - } catch (e: Exception) { - // Skip invalid JSON lines - continue - } - } + executor to llmModel } - } else { - emit( - "{\"error\":\"Ollama request failed: ${response.status} - Check if Ollama server is running at $baseUrl\"}" - ) + + else -> throw IllegalArgumentException("Unsupported LLM provider: $provider") } - } catch (e: Exception) { - emit("{\"error\":\"Failed to connect to Ollama server at $baseUrl: ${e.message}\"}") - } + + // Create the agent + val agent = + AIAgent( + promptExecutor = executor, + llmModel = llmModel, + systemPrompt = + """ + You are a helpful assistant for code analysis using the Code Property Graph (CPG). + You have access to various CPG analysis tools through MCP. + Use these tools proactively to analyze code and answer questions about code structure. + + When multiple independent tools are needed, call them in a single turn if possible. + """ + .trimIndent(), + toolRegistry = toolRegistry, + maxIterations = 100, + ) + + // Send initial empty data to establish the SSE connection. + send("") + + val result = agent.run(userMessage) + + println("Debug: Agent completed with result length: ${result.length}") + + // Send the complete result + send(result) + send("[DONE]") } } diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ConsoleService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ConsoleService.kt index 94c84ec8162..ef715de2ad6 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ConsoleService.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ConsoleService.kt @@ -35,6 +35,7 @@ import de.fraunhofer.aisec.cpg.graph.concepts.Concept import de.fraunhofer.aisec.cpg.graph.concepts.conceptBuildHelper import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration import de.fraunhofer.aisec.cpg.graph.nodes +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.globalAnalysisResult import de.fraunhofer.aisec.cpg.passes.concepts.LoadPersistedConcepts import de.fraunhofer.aisec.cpg.passes.concepts.LoadPersistedConcepts.PersistedConceptEntry import de.fraunhofer.aisec.cpg.passes.concepts.LoadPersistedConcepts.PersistedConcepts @@ -309,6 +310,8 @@ class ConsoleService { companion object { /** Creates a new [ConsoleService] instance from the given [AnalysisResult]. */ fun fromAnalysisResult(result: AnalysisResult): ConsoleService { + // TODO: Might lead to an error when mcp module not enabled + globalAnalysisResult = result.translationResult val service = ConsoleService() service.analysisResult = result.toJSON() service.lastProject = result.project diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt index 95c7d1fa423..c51296f3665 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt @@ -25,6 +25,8 @@ */ package de.fraunhofer.aisec.codyze.console +import de.fraunhofer.aisec.cpg.mcp.mcpserver.configureServer +import de.fraunhofer.aisec.cpg.mcp.runSseMcpServerUsingKtorPlugin import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* @@ -33,6 +35,7 @@ import io.ktor.server.netty.* import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.plugins.cors.routing.* import io.ktor.server.routing.* +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json /** @@ -45,10 +48,20 @@ fun ConsoleService.startConsole( port: Int = 8080, chatService: ChatService = ChatService(), ) { - embeddedServer(Netty, host = host, port = port) { - configureWebconsole(this@startConsole, chatService) - } - .start(wait = true) + // TODO(): MCP server should only run when cpg-mcp module enabled + runBlocking { + // Start MCP server in background (wait = false means it won't block) + println("Starting MCP server on port 8081...") + runSseMcpServerUsingKtorPlugin(8081, configureServer()) + + // Start main server (also with wait = false to avoid blocking) + println("Starting main server on port 8080...") + val mainServer = + embeddedServer(Netty, host = host, port = port) { + configureWebconsole(this@startConsole, chatService) + } + mainServer.start(wait = true) + } } /** diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt index 39bb3c7e685..72be5e7526f 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt @@ -246,19 +246,17 @@ fun Routing.apiRoutes(service: ConsoleService, chatService: ChatService) { } post("/chat") { - try { - val request = call.receive() - call.respondTextWriter(contentType = ContentType.Text.EventStream) { + val request = call.receive() + call.respondTextWriter(contentType = ContentType.Text.EventStream) { + try { chatService.chat(request).collect { chunk -> - write("data: $chunk\n\n") + chunk.split("\n").forEach { line -> write("data: $line\n") } + write("\n") // Empty line marks end of event flush() } + } catch (e: Exception) { + e.printStackTrace() } - } catch (e: Exception) { - call.respond( - HttpStatusCode.InternalServerError, - mapOf("error" to "Chat request failed: ${e.message}"), - ) } } diff --git a/codyze-console/src/main/resources/application.conf b/codyze-console/src/main/resources/application.conf index d7d8329a150..afd87b89437 100644 --- a/codyze-console/src/main/resources/application.conf +++ b/codyze-console/src/main/resources/application.conf @@ -1,12 +1,22 @@ llm { - # LLM Configuration - provider = "OLLAMA" # Options: OLLAMA, OPENAI, ANTHROPIC - baseUrl = "http://172.31.6.131:11434" - model = "gpt-oss:120b" - apiKey = "" # Required for OPENAI and ANTHROPIC, optional for OLLAMA + provider = "ollama" # Options: "ollama", "gemini", vLLM + + gemini { + model = "gemini-2.0-flash-exp" + } + + ollama { + baseUrl = "http://172.31.6.131:11434" + model = "gpt-oss:120b" + } + + vLLM { + baseUrl = "http://172.31.6.131:8000/v1/chat/completions" + model = "zai-org/GLM-4.5-Air-FP8" + } } mcp { # MCP Server Configuration - serverUrl = "http://localhost:8081" + serverUrl = "http://localhost:8081/sse" } \ No newline at end of file diff --git a/codyze-console/src/main/webapp/package.json b/codyze-console/src/main/webapp/package.json index dc5a0d15297..618126dd0fa 100644 --- a/codyze-console/src/main/webapp/package.json +++ b/codyze-console/src/main/webapp/package.json @@ -41,6 +41,7 @@ "vite": "^6.0.0" }, "dependencies": { - "chart.js": "^4.4.9" + "chart.js": "^4.4.9", + "marked": "^16.3.0" } } diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index ba3ae264b76..d1b0acb897d 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: chart.js: specifier: ^4.4.9 version: 4.5.0 + marked: + specifier: ^16.3.0 + version: 16.3.0 devDependencies: '@eslint/compat': specifier: ^1.2.5 @@ -1162,6 +1165,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked@16.3.0: + resolution: {integrity: sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==} + engines: {node: '>= 20'} + hasBin: true + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2496,6 +2504,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@16.3.0: {} + merge2@1.4.1: {} micromatch@4.0.8: diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/ChatInterface.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/ChatInterface.svelte index 7cbdb47ada1..424d08b33ed 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/ChatInterface.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/ChatInterface.svelte @@ -1,6 +1,9 @@ -
- -
- -
-
-
- - - -
-
-

AI Assistant

-

Analyzing {selectedComponent.name}

-
-
- -
+
+ +
+ +
- -
+ +
+
{#each messages as message} -
-
-
- -
- {#if message.role === 'user'} - - - - {:else} - - - - {/if} -
- - -
-
-
{message.content}
-
- -
- {new Intl.DateTimeFormat('de-DE', { - hour: '2-digit', - minute: '2-digit', - day: '2-digit', - month: '2-digit' - }).format(message.timestamp)} -
+ {#if message.role === 'user'} + +
+
+
+
{message.content}
-
+ {:else} + +
+
+ +
+
+ {/if} {/each} - {#if isLoading || streamingContent} -
-
-
-
- - - + {#if isLoading || displayContent} + +
+ {#if displayContent} +
+
+ {displayContent}
- - -
- {#if streamingContent} -
- {streamingContent}ā–Š -
- {:else} -
-
-
-
-
-
-
- {/if} +
+ {:else} + +
+
+
+
+
-
+ {/if}
{/if}
- - -
-
- - -
-
- -
- -
- +
+
+
- - -
- {#if selectedUnit} - - {:else} -
-
- - - -

No file selected

-

- Select a translation unit from the sidebar to view its contents -

-
-
- {/if} -
diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/MarkdownRenderer.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/MarkdownRenderer.svelte new file mode 100644 index 00000000000..89df66e0713 --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/MarkdownRenderer.svelte @@ -0,0 +1,149 @@ + + +
+ {@html html} +
+ + diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/MessageInput.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/MessageInput.svelte new file mode 100644 index 00000000000..9a438976c7b --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/MessageInput.svelte @@ -0,0 +1,66 @@ + + +
+ + +
\ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/WelcomeScreen.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/WelcomeScreen.svelte index 8297dfd1184..8ea3a79a4e6 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/WelcomeScreen.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/WelcomeScreen.svelte @@ -1,124 +1,93 @@ -
- -
-
- šŸ‘‹ -

- Hi, I'm your CodAIze Assistant -
-
- - - -
-
-

+
+ +
+
+ šŸ‘‹ +

+ Hi, I'm your CodAIze Assistant +
+
+ + + +
- -

- I help you find security vulnerabilities, understand data flows, and analyze code dependencies using graph-based analysis. -

+

- - {#if components.length > 0} -
-
-

Choose a component to analyze

-
- {#each components as component} - - {/each} -
-
-
- {/if} - - -
-

Popular CPG analysis queries:

-
- {#each suggestedQuestions as question} - - {/each} -
-
+

+ I help you find security vulnerabilities, understand data flows, and analyze code dependencies + using graph-based analysis. +

+
- -
- e.key === 'Enter' && handleSendMessage()} - /> + +
+
+ {#each suggestedQuestions as question} + type="button" + class="group rounded-2xl border border-gray-200 bg-white p-4 text-left shadow-sm transition-all duration-200 hover:border-blue-400 hover:bg-gradient-to-br hover:from-blue-50 hover:to-purple-50 hover:shadow-md" + onclick={() => onWelcomeMessage(question)} + > +
+
+ + + +
+ {question} +
+ + {/each}
+
+ + +
+ messageInput = value} + placeholder="Ask me anything about your codebase..." + /> +
diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/index.ts b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/index.ts index 411a999e4d8..c78091219d7 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/index.ts +++ b/codyze-console/src/main/webapp/src/lib/components/ai-assistant/index.ts @@ -1,3 +1,4 @@ export { default as WelcomeScreen } from './WelcomeScreen.svelte'; export { default as ChatInterface } from './ChatInterface.svelte'; - +export { default as MarkdownRenderer } from './MarkdownRenderer.svelte'; +export { default as MessageInput } from './MessageInput.svelte'; diff --git a/codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte b/codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte index 56f54c5b880..dd4702b5bd0 100644 --- a/codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte @@ -75,7 +75,7 @@ }); -
+
diff --git a/codyze-console/src/main/webapp/src/lib/components/analysis/FileTree.svelte b/codyze-console/src/main/webapp/src/lib/components/analysis/FileTree.svelte index 8c92aa9d73e..78326c4aa9e 100644 --- a/codyze-console/src/main/webapp/src/lib/components/analysis/FileTree.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/analysis/FileTree.svelte @@ -91,7 +91,7 @@
-

Files

+

Files

{#snippet TreeNode(nodes: TreeNode[], depth = 0)}
    @@ -106,12 +106,20 @@ > - + - + {node.name} @@ -131,14 +141,17 @@
diff --git a/codyze-console/src/main/webapp/src/lib/components/analysis/QueryTreeExplorer.svelte b/codyze-console/src/main/webapp/src/lib/components/analysis/QueryTreeExplorer.svelte index 04ed8bab29f..93d54459322 100644 --- a/codyze-console/src/main/webapp/src/lib/components/analysis/QueryTreeExplorer.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/analysis/QueryTreeExplorer.svelte @@ -194,8 +194,11 @@ // Function to load more node values for display function loadMoreNodes() { if (!queryTree?.nodeValues) return; - - const newCount = Math.min(displayedNodeCount + NODE_VALUES_BATCH_SIZE, queryTree.nodeValues.length); + + const newCount = Math.min( + displayedNodeCount + NODE_VALUES_BATCH_SIZE, + queryTree.nodeValues.length + ); displayedNodeCount = newCount; if (newCount >= queryTree.nodeValues.length) { diff --git a/codyze-console/src/main/webapp/src/lib/services/apiService.ts b/codyze-console/src/main/webapp/src/lib/services/apiService.ts index 52d34120d62..383352a5362 100644 --- a/codyze-console/src/main/webapp/src/lib/services/apiService.ts +++ b/codyze-console/src/main/webapp/src/lib/services/apiService.ts @@ -1,122 +1,140 @@ interface ApiResponse { - ok: boolean; - status: number; - statusText: string; - data?: T; - error?: string; + ok: boolean; + status: number; + statusText: string; + data?: T; + error?: string; } class ApiService { - constructor(private readonly baseUrl: string = "") { + constructor(private readonly baseUrl: string = '') {} + + private async request(url: string, options: RequestInit): Promise> { + try { + const response = await fetch(`${this.baseUrl}${url}`, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(options.headers ?? {}) + }, + ...options + }); + + const data = response.ok ? await response.json() : undefined; + + return { + ok: response.ok, + status: response.status, + statusText: response.statusText, + data, + error: response.ok ? undefined : `HTTP ${response.status}: ${response.statusText}` + }; + } catch (err) { + return { + ok: false, + status: 0, + statusText: 'Network Error', + error: err instanceof Error ? err.message : String(err) + }; } + } + + get(url: string, headers: Record = {}) { + return this.request(url, { method: 'GET', headers }); + } + + post(url: string, body: any, headers: Record = {}) { + return this.request(url, { + method: 'POST', + headers, + body: JSON.stringify(body) + }); + } - private async request( - url: string, - options: RequestInit - ): Promise> { - try { - const response = await fetch(`${this.baseUrl}${url}`, { - headers: { - "Content-Type": "application/json", - Accept: "application/json", - ...(options.headers ?? {}) - }, - ...options - }); - - const data = response.ok ? await response.json() : undefined; - - return { - ok: response.ok, - status: response.status, - statusText: response.statusText, - data, - error: response.ok ? undefined : `HTTP ${response.status}: ${response.statusText}` - }; - } catch (err) { - return { - ok: false, - status: 0, - statusText: "Network Error", - error: err instanceof Error ? err.message : String(err) - }; + async streamPost( + url: string, + body: any, + onChunk: (chunk: string) => void, + onError?: (error: string) => void, + onComplete?: () => void + ): Promise { + try { + const response = await fetch(`${this.baseUrl}${url}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + 'Cache-Control': 'no-cache' + }, + body: JSON.stringify(body) + }); + + if (!response.ok) { + onError?.(`HTTP ${response.status}: ${response.statusText}`); + return; + } + + const reader = response.body?.getReader(); + if (!reader) { + onError?.('Failed to get response reader'); + return; + } + + const decoder = new TextDecoder(); + let buffer = ''; + let eventDataLines: string[] = []; // Persist across reads + + while (true) { + const { done, value } = await reader.read(); + if (done) { + onComplete?.(); + break; } - } - get(url: string, headers: Record = {}) { - return this.request(url, {method: "GET", headers}); - } + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; - post(url: string, body: any, headers: Record = {}) { - return this.request(url, { - method: "POST", - headers, - body: JSON.stringify(body) - }); - } + // SSE spec: collect all "data:" lines until empty line, then emit as one event + for (let raw of lines) { + const line = raw.replace(/\r?$/, ''); - // TODO: Refactor this with the new post api - async streamPost(url: string, body: any, onChunk: (chunk: string) => void, onError?: (error: string) => void, onComplete?: () => void): Promise { - try { - const response = await fetch(`${this.baseUrl}${url}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'text/event-stream', - 'Cache-Control': 'no-cache' - }, - body: JSON.stringify(body) - }); - - if (!response.ok) { - if (onError) { - onError(`HTTP ${response.status}: ${response.statusText}`); - } - return; - } + // Skip comments + if (line.startsWith(':')) continue; + + // Empty line = end of event + if (line === '') { + if (eventDataLines.length > 0) { + // Join all data lines with \n and emit + const eventData = eventDataLines.join('\n'); + eventDataLines = []; - const reader = response.body?.getReader(); - if (!reader) { - if (onError) { - onError('Failed to get response reader'); - } + if (eventData === '[DONE]') { + onComplete?.(); return; - } + } - const decoder = new TextDecoder(); - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - - if (done) { - if (onComplete) { - onComplete(); - } - break; - } - - buffer += decoder.decode(value, { stream: true }); - - // Process SSE data - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; // Keep incomplete line in buffer - - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(6); // Remove 'data: ' prefix - if (data.trim()) { - onChunk(data); - } - } - } - } - } catch (error) { - if (onError) { - onError(error instanceof Error ? error.message : String(error)); + if (eventData.startsWith('ERROR:')) { + onError?.(eventData.substring('ERROR:'.length).trim()); + continue; + } + + onChunk(eventData); } + continue; + } + + // Collect data lines + if (line.startsWith('data:')) { + const data = line.startsWith('data: ') ? line.slice(6) : line.slice(5).trimStart(); + eventDataLines.push(data); + } } + } + } catch (error) { + onError?.(error instanceof Error ? error.message : String(error)); } + } } export default ApiService; \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts b/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts index ff94b0714fa..d81da38e983 100644 --- a/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts +++ b/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts @@ -1,30 +1,35 @@ import ApiService from './apiService'; export interface LLMMessage { - role: 'user' | 'assistant' | 'system'; - content: string; + role: 'user' | 'assistant' | 'system'; + content: string; } export interface StreamingCallbacks { - onChunk: (chunk: string) => void; - onError?: (error: string) => void; - onComplete?: () => void; + onChunk: (chunk: string) => void; + onError?: (error: string) => void; + onComplete?: () => void; } class LLMAgent { - private apiService = new ApiService(); + private apiService = new ApiService(); - async chat(messages: LLMMessage[], callbacks: StreamingCallbacks): Promise { - const requestData = { - messages: messages.map(msg => ({ - role: msg.role, - content: msg.content - })) - }; + async chat(messages: LLMMessage[], callbacks: StreamingCallbacks): Promise { + const requestData = { + messages: messages.map((msg) => ({ + role: msg.role, + content: msg.content + })) + }; - await this.apiService.streamPost('/api/chat', requestData, callbacks.onChunk, callbacks.onError, callbacks.onComplete); - } + await this.apiService.streamPost( + '/api/chat', + requestData, + callbacks.onChunk, + callbacks.onError, + callbacks.onComplete + ); + } } export const llmAgent = new LLMAgent(); - diff --git a/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.svelte b/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.svelte index a6e72bd4cc1..441668c14bd 100644 --- a/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.svelte +++ b/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.svelte @@ -2,53 +2,67 @@ import type { PageProps } from './$types'; import { WelcomeScreen, ChatInterface } from '$lib/components/ai-assistant'; import { PageHeader } from '$lib/components/navigation'; - import { llmAgent, type LLMMessage, type StreamingCallbacks } from '$lib/services/llmAgent'; - import type { ComponentJSON } from '$lib/types'; + import { + llmAgent, + type LLMMessage, + type StreamingCallbacks, + } from '$lib/services/llmAgent'; let { data }: PageProps = $props(); - const components = $derived(data?.components || []); - const hasError = $derived(!data || !data.components); + const hasError = $derived(!data); - let chatMessages = $state>([]); + // Load persisted state from sessionStorage + function loadPersistedState() { + if (typeof window === 'undefined') return { messages: [], showWelcome: true }; + + const stored = sessionStorage.getItem('ai-assistant-state'); + if (!stored) return { messages: [], showWelcome: true }; + + try { + const parsed = JSON.parse(stored); + const messages = parsed.messages.map((msg: any) => ({ + ...msg, + timestamp: new Date(msg.timestamp) + })); + return { messages, showWelcome: parsed.showWelcome }; + } catch { + return { messages: [], showWelcome: true }; + } + } + + const persisted = loadPersistedState(); + + let chatMessages = $state< + Array<{ id: string; role: 'user' | 'assistant'; content: string; timestamp: Date }> + >(persisted.messages); let currentMessage = $state(''); let isLoading = $state(false); let streamingContent = $state(''); - let selectedComponent = $state(null); - let showWelcome = $state(true); + let visibleStreamingContent = $state(''); + let showWelcome = $state(persisted.showWelcome); + + // Save state to sessionStorage whenever it changes + $effect(() => { + if (typeof window !== 'undefined') { + sessionStorage.setItem('ai-assistant-state', JSON.stringify({ + messages: chatMessages, + showWelcome + })); + } + }); - function selectComponent(component: ComponentJSON) { - selectedComponent = component; - showWelcome = false; - chatMessages = [{ - id: '1', - role: 'assistant', - content: `Hi! I'm analyzing the **${component.name}** component. What would you like to know about it?`, - timestamp: new Date() - }]; - } function handleWelcomeMessage(message: string) { currentMessage = message; showWelcome = false; - // Need to select the first available component when using welcome message - if (components.length > 0) { - selectedComponent = components[0]; - } else { - // TODO: Not sure whether we need the component handling - selectedComponent = { - name: "No Components", - translationUnits: [], - topLevel: null - }; - } sendMessage(); } function resetChat() { showWelcome = true; - selectedComponent = null; chatMessages = []; currentMessage = ''; + streamingContent = ''; } async function sendMessage() { @@ -67,14 +81,21 @@ streamingContent = ''; try { - const llmMessages: LLMMessage[] = chatMessages.map(msg => ({ + const llmMessages: LLMMessage[] = chatMessages.map((msg) => ({ role: msg.role as 'user' | 'assistant', content: msg.content })); const callbacks: StreamingCallbacks = { onChunk: (chunk: string) => { + // Append incoming chunk to streaming content directly streamingContent += chunk; + + // Only show visible content if we have non-whitespace characters + // This preserves the typing animation at the start + if (streamingContent.trim().length > 0) { + visibleStreamingContent = streamingContent; + } }, onError: (error: string) => { console.error('Streaming error:', error); @@ -87,20 +108,24 @@ chatMessages = [...chatMessages, errorMessage]; isLoading = false; streamingContent = ''; + visibleStreamingContent = ''; }, onComplete: () => { - // When streaming is complete, add the full message to chat history - if (streamingContent) { + const content = streamingContent; + if (content && content.trim().length > 0) { const assistantMessage = { id: (Date.now() + 1).toString(), role: 'assistant' as const, - content: streamingContent, + content, timestamp: new Date() }; chatMessages = [...chatMessages, assistantMessage]; + } else { + console.warn('Stream complete but no content to append'); } isLoading = false; streamingContent = ''; + visibleStreamingContent = ''; } }; @@ -119,39 +144,47 @@ } -
- + + + +
+{#if hasError} +
+ + + +

Service unavailable

+

+ Could not connect to the analysis service. Please ensure the backend is running. +

+
+{:else if showWelcome}
- {#if hasError} -
- - - -

No components available

-

- Could not load analysis components. Please ensure your project has been analyzed and the backend service is running. -

-
- {:else} - {#if showWelcome} - - {:else if selectedComponent} - currentMessage = message} - /> - {/if} - {/if} +
+{:else} + (currentMessage = message)} + /> +{/if}
diff --git a/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.ts b/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.ts index 82b73e3a9eb..69db4aa9dfc 100644 --- a/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.ts +++ b/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.ts @@ -1,16 +1,7 @@ import type { PageLoad } from './$types'; export const load: PageLoad = async ({ fetch }) => { - try { - const response = await fetch('/api/result'); - const result = await response.json(); - return { - components: result.components || [] - }; - } catch (error) { - console.error('Failed to load components:', error); - return { - components: null - }; - } + return { + components: [] + }; }; diff --git a/codyze-console/src/main/webapp/vite.config.ts b/codyze-console/src/main/webapp/vite.config.ts index fb52cb9104f..65a884eadc8 100644 --- a/codyze-console/src/main/webapp/vite.config.ts +++ b/codyze-console/src/main/webapp/vite.config.ts @@ -6,11 +6,17 @@ export default defineConfig({ plugins: [tailwindcss(), sveltekit()], server: { proxy: { - '/api': 'http://localhost:8080', - '/ollama': { - target: 'http://localhost:11434', + '/api': { + target: 'http://localhost:8080', changeOrigin: true, - rewrite: (path) => path.replace(/^\/ollama/, '') + timeout: 0, + proxyTimeout: 0, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq) => { + // Ensure connection stays alive for SSE + proxyReq.setHeader('Connection', 'keep-alive'); + }); + } } } } diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt index 2311406ad03..2821fc5ee6d 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt @@ -64,7 +64,7 @@ class McpServer : CliktCommand(name = "mcp-server") { val port = ssePort if (port != null) { println("Starting MCP server in SSE mode on port $port...") - runSseMcpServerUsingKtorPlugin(port, configureServer()) + runBlocking { runSseMcpServerUsingKtorPlugin(port, configureServer()) } } else { println("Starting MCP server in stdio mode...") runMcpServerUsingStdio() @@ -91,6 +91,6 @@ fun runMcpServerUsingStdio() { * * @param port The port number on which the SSE MCP server will listen for client connections. */ -fun runSseMcpServerUsingKtorPlugin(port: Int, server: Server) = runBlocking { - embeddedServer(CIO, host = "0.0.0.0", port = port) { mcp { server } }.start(wait = true) +suspend fun runSseMcpServerUsingKtorPlugin(port: Int, server: Server) { + embeddedServer(CIO, host = "0.0.0.0", port = port) { mcp { server } }.start(wait = false) } diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt index 8c57488f928..389dc648f87 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt @@ -36,6 +36,7 @@ import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration import de.fraunhofer.aisec.cpg.graph.listOverlayClasses import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.globalAnalysisResult +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.runCpgAnalyze import de.fraunhofer.aisec.cpg.query.QueryTree import io.modelcontextprotocol.kotlin.sdk.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.CallToolResult @@ -43,6 +44,7 @@ import io.modelcontextprotocol.kotlin.sdk.TextContent import java.util.function.BiFunction import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive fun Node.toNodeInfo(): NodeInfo { return NodeInfo(this) @@ -97,17 +99,30 @@ fun CallToolRequest.runOnCpg( query: BiFunction ): CallToolResult { return try { - val result = - globalAnalysisResult - ?: return CallToolResult( - content = - listOf( - TextContent( - "No analysis result available. Please analyze your code first using cpg_analyze." - ) + var result = globalAnalysisResult + + if (result == null) { + val content = + this.arguments["content"]?.let { if (it is JsonPrimitive) it.content else null } + val extension = + this.arguments["extension"]?.let { if (it is JsonPrimitive) it.content else null } + + if (content != null && extension != null) { + val payload = CpgAnalyzePayload(content = content, extension = extension) + runCpgAnalyze(payload) + result = globalAnalysisResult + } + } + + result?.let { query.apply(it, this) } + ?: CallToolResult( + content = + listOf( + TextContent( + "No analysis result available. Either run cpg_analyze first or provide 'content' and 'extension' parameters." ) - ) - query.apply(result, this) + ) + ) } catch (e: Exception) { CallToolResult( content = diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a455373272..664cddd96cd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ sarif4k = "0.6.0" ktor = "3.3.0" node = "22.14.0" deno-plugin = "0.1.5" +koog = "0.5.0" [libraries] kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin"} @@ -26,6 +27,11 @@ kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", v kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-json" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.10.2"} +koog-agents = { module = "ai.koog:koog-agents", version.ref = "koog" } +koog-tools = { module = "ai.koog:agents-tools", version.ref = "koog" } +koog-executor-ollama-client = { module = "ai.koog:prompt-executor-ollama-client", version.ref = "koog" } +koog-executor-google-client = { module = "ai.koog:prompt-executor-google-client", version.ref = "koog" } + reflections = { module = "org.reflections:reflections", version = "0.10.2"} log4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" } From 2653e86660145cea94b478881519f5efa5788664 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 16 Dec 2025 13:42:51 +0100 Subject: [PATCH 40/93] feat(console): rename to CodAIze Agent, update sidebar icon to robot, gradient styling, route /ai-agent --- ...kotlin-compiler-7977987601759287845.salive | 0 codyze-console/build.gradle.kts | 14 +- .../aisec/codyze/console/ChatService.kt | 153 ------ .../aisec/codyze/console/ConsoleService.kt | 9 +- .../fraunhofer/aisec/codyze/console/Main.kt | 54 +- .../fraunhofer/aisec/codyze/console/Nodes.kt | 88 +--- .../fraunhofer/aisec/codyze/console/Router.kt | 77 ++- .../aisec/codyze/console/ai/ChatService.kt | 244 +++++++++ .../codyze/console/ai/CustomMcpClient.kt | 466 ++++++++++++++++++ .../codyze/console/ai/McpServerHelper.kt | 71 +++ .../src/main/resources/application.conf | 4 +- .../ChatInterface.svelte | 157 +++++- .../components/ai-agent/CodePreview.svelte | 212 ++++++++ .../MarkdownRenderer.svelte | 0 .../MessageInput.svelte | 0 .../components/ai-agent/ResultsList.svelte | 203 ++++++++ .../WelcomeScreen.svelte | 2 +- .../{ai-assistant => ai-agent}/index.ts | 2 + .../lib/components/analysis/CodeViewer.svelte | 109 +++- .../lib/components/navigation/Sidebar.svelte | 134 +++-- .../main/webapp/src/lib/services/llmAgent.ts | 32 ++ .../{ai-assistant => ai-agent}/+page.svelte | 97 +++- .../main/webapp/src/routes/ai-agent/+page.ts | 29 ++ .../webapp/src/routes/ai-assistant/+page.ts | 7 - .../aisec/codyze/console/ApplicationTest.kt | 1 + cpg-mcp/build.gradle.kts | 4 + .../kotlin/mcp/ListCommandsTest.kt | 27 +- .../fraunhofer/aisec/cpg/mcp/Application.kt | 12 +- .../aisec/cpg/mcp/mcpserver/McpServer.kt | 5 - .../cpg/mcp/mcpserver/tools/CpgAnalyzeTool.kt | 12 +- .../mcpserver/tools/CpgApplyConceptsTool.kt | 10 +- .../mcp/mcpserver/tools/CpgDataflowTool.kt | 9 +- .../mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt | 73 ++- .../cpg/mcp/mcpserver/tools/ListCommands.kt | 29 +- .../mcp/mcpserver/tools/utils/Serializable.kt | 191 +------ .../cpg/mcp/mcpserver/tools/utils/Util.kt | 33 +- cpg-serialization/build.gradle.kts | 41 ++ .../cpg/serialization/NodeSerialization.kt | 116 +++++ gradle/libs.versions.toml | 5 +- settings.gradle.kts | 1 + 40 files changed, 2073 insertions(+), 660 deletions(-) create mode 100644 .kotlin/sessions/kotlin-compiler-7977987601759287845.salive delete mode 100644 codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt create mode 100644 codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt create mode 100644 codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/CustomMcpClient.kt create mode 100644 codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpServerHelper.kt rename codyze-console/src/main/webapp/src/lib/components/{ai-assistant => ai-agent}/ChatInterface.svelte (54%) create mode 100644 codyze-console/src/main/webapp/src/lib/components/ai-agent/CodePreview.svelte rename codyze-console/src/main/webapp/src/lib/components/{ai-assistant => ai-agent}/MarkdownRenderer.svelte (100%) rename codyze-console/src/main/webapp/src/lib/components/{ai-assistant => ai-agent}/MessageInput.svelte (100%) create mode 100644 codyze-console/src/main/webapp/src/lib/components/ai-agent/ResultsList.svelte rename codyze-console/src/main/webapp/src/lib/components/{ai-assistant => ai-agent}/WelcomeScreen.svelte (99%) rename codyze-console/src/main/webapp/src/lib/components/{ai-assistant => ai-agent}/index.ts (68%) rename codyze-console/src/main/webapp/src/routes/{ai-assistant => ai-agent}/+page.svelte (60%) create mode 100644 codyze-console/src/main/webapp/src/routes/ai-agent/+page.ts delete mode 100644 codyze-console/src/main/webapp/src/routes/ai-assistant/+page.ts create mode 100644 cpg-serialization/build.gradle.kts create mode 100644 cpg-serialization/src/main/kotlin/de/fraunhofer/aisec/cpg/serialization/NodeSerialization.kt diff --git a/.kotlin/sessions/kotlin-compiler-7977987601759287845.salive b/.kotlin/sessions/kotlin-compiler-7977987601759287845.salive new file mode 100644 index 00000000000..e69de29bb2d diff --git a/codyze-console/build.gradle.kts b/codyze-console/build.gradle.kts index 5a8afe42a60..c1788102c71 100644 --- a/codyze-console/build.gradle.kts +++ b/codyze-console/build.gradle.kts @@ -14,11 +14,21 @@ mavenPublishing { } } +val mcpEnabled = findProject(":cpg-mcp") != null + dependencies { // CPG modules implementation(projects.cpgConcepts) - implementation(projects.cpgMcp) - implementation(libs.mcp) + implementation(projects.cpgSerialization) + + // MCP dependencies - only when enabled + if (mcpEnabled) { + implementation(project(":cpg-mcp")) + // MCP SDK - needed for McpServerHelper and custom MCP client + implementation(libs.mcp) + // MCP Client SDK - for custom MCP client implementation + implementation(libs.mcp.client) + } // Ktor server dependencies implementation(libs.bundles.ktor) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt deleted file mode 100644 index d3ff95491f3..00000000000 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ChatService.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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.console - -import ai.koog.agents.core.agent.AIAgent -import ai.koog.agents.mcp.McpToolRegistryProvider -import ai.koog.prompt.executor.llms.all.simpleGoogleAIExecutor -import ai.koog.prompt.executor.llms.all.simpleOllamaAIExecutor -import ai.koog.prompt.llm.LLMCapability -import ai.koog.prompt.llm.LLMProvider -import ai.koog.prompt.llm.LLModel -import com.typesafe.config.ConfigFactory -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.serialization.Serializable - -@Serializable data class ChatMessageJSON(val role: String, val content: String) - -@Serializable data class ChatRequestJSON(val messages: List) - -class ChatService() { - val config = ConfigFactory.load() - val provider: String = config.getString("llm.provider") - val mcpServerUrl: String = config.getString("mcp.serverUrl") - - fun chat(request: ChatRequestJSON): Flow = channelFlow { - val userMessage = request.messages.lastOrNull()?.content ?: "" - - // Connect to MCP server via SSE - val transport = McpToolRegistryProvider.defaultSseTransport(mcpServerUrl) - - // Create tool registry from MCP server - val toolRegistry = - McpToolRegistryProvider.fromTransport( - transport = transport, - name = "codyze-console", - version = "1.0.0", - ) - - // Get provider-specific configuration - val (executor, llmModel) = - when (provider.lowercase()) { - "gemini" -> { - val apiKey = - System.getenv("GEMINI_API_KEY") - ?: throw IllegalStateException( - "GEMINI_API_KEY environment variable not set" - ) - val model = config.getString("llm.gemini.model") - - val executor = simpleGoogleAIExecutor(apiKey = apiKey) - val llmModel = - LLModel( - provider = LLMProvider.Google, - id = model, - capabilities = listOf(LLMCapability.Temperature, LLMCapability.Tools), - contextLength = 128000, - maxOutputTokens = 8192, - ) - - executor to llmModel - } - - "ollama" -> { - val baseUrl = config.getString("llm.ollama.baseUrl") - val model = config.getString("llm.ollama.model") - - val executor = simpleOllamaAIExecutor(baseUrl = baseUrl) - val llmModel = - LLModel( - provider = LLMProvider.Ollama, - id = model, - capabilities = listOf(LLMCapability.Tools), - contextLength = 65536, - maxOutputTokens = 8192, - ) - - executor to llmModel - } - - "vLLM" -> { - val baseUrl = config.getString("llm.vLLM.baseUrl") - val model = config.getString("llm.vLLM.model") - - val executor = simpleOllamaAIExecutor(baseUrl = baseUrl) - val llmModel = - LLModel( - provider = LLMProvider.OpenAI, - id = model, - capabilities = listOf(LLMCapability.Tools), - contextLength = 65536, - maxOutputTokens = 8192, - ) - - executor to llmModel - } - - else -> throw IllegalArgumentException("Unsupported LLM provider: $provider") - } - - // Create the agent - val agent = - AIAgent( - promptExecutor = executor, - llmModel = llmModel, - systemPrompt = - """ - You are a helpful assistant for code analysis using the Code Property Graph (CPG). - You have access to various CPG analysis tools through MCP. - Use these tools proactively to analyze code and answer questions about code structure. - - When multiple independent tools are needed, call them in a single turn if possible. - """ - .trimIndent(), - toolRegistry = toolRegistry, - maxIterations = 100, - ) - - // Send initial empty data to establish the SSE connection. - send("") - - val result = agent.run(userMessage) - - println("Debug: Agent completed with result length: ${result.length}") - - // Send the complete result - send(result) - send("[DONE]") - } -} diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ConsoleService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ConsoleService.kt index ef715de2ad6..c5894382441 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ConsoleService.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ConsoleService.kt @@ -30,17 +30,19 @@ import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import de.fraunhofer.aisec.codyze.AnalysisProject import de.fraunhofer.aisec.codyze.AnalysisResult +import de.fraunhofer.aisec.codyze.console.ai.McpServerHelper import de.fraunhofer.aisec.cpg.TranslationConfiguration import de.fraunhofer.aisec.cpg.graph.concepts.Concept import de.fraunhofer.aisec.cpg.graph.concepts.conceptBuildHelper import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration import de.fraunhofer.aisec.cpg.graph.nodes -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.globalAnalysisResult import de.fraunhofer.aisec.cpg.passes.concepts.LoadPersistedConcepts import de.fraunhofer.aisec.cpg.passes.concepts.LoadPersistedConcepts.PersistedConceptEntry import de.fraunhofer.aisec.cpg.passes.concepts.LoadPersistedConcepts.PersistedConcepts import de.fraunhofer.aisec.cpg.passes.concepts.config.python.PythonStdLibConfigurationPass import de.fraunhofer.aisec.cpg.query.QueryTree +import de.fraunhofer.aisec.cpg.serialization.NodeJSON +import de.fraunhofer.aisec.cpg.serialization.toJSON import java.io.File import java.nio.file.Path import kotlin.uuid.Uuid @@ -126,6 +128,9 @@ class ConsoleService { val result = project.analyze() + // Update the global analysis result in the MCP server + McpServerHelper.setGlobalAnalysisResult(result.translationResult) + // Populate QueryTree cache for lazy loading populateQueryTreeCache(result.requirementsResults) @@ -310,8 +315,6 @@ class ConsoleService { companion object { /** Creates a new [ConsoleService] instance from the given [AnalysisResult]. */ fun fromAnalysisResult(result: AnalysisResult): ConsoleService { - // TODO: Might lead to an error when mcp module not enabled - globalAnalysisResult = result.translationResult val service = ConsoleService() service.analysisResult = result.toJSON() service.lastProject = result.project diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt index c51296f3665..ae9628ff36d 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt @@ -25,8 +25,9 @@ */ package de.fraunhofer.aisec.codyze.console -import de.fraunhofer.aisec.cpg.mcp.mcpserver.configureServer -import de.fraunhofer.aisec.cpg.mcp.runSseMcpServerUsingKtorPlugin +import de.fraunhofer.aisec.codyze.console.ai.ChatService +import de.fraunhofer.aisec.codyze.console.ai.CustomMcpClient +import de.fraunhofer.aisec.codyze.console.ai.McpServerHelper import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* @@ -46,22 +47,37 @@ import kotlinx.serialization.json.Json fun ConsoleService.startConsole( host: String = "localhost", port: Int = 8080, - chatService: ChatService = ChatService(), + chatService: ChatService? = if (McpServerHelper.isEnabled) ChatService() else null, ) { - // TODO(): MCP server should only run when cpg-mcp module enabled - runBlocking { - // Start MCP server in background (wait = false means it won't block) - println("Starting MCP server on port 8081...") - runSseMcpServerUsingKtorPlugin(8081, configureServer()) + var customMcpClient: CustomMcpClient? = null - // Start main server (also with wait = false to avoid blocking) - println("Starting main server on port 8080...") - val mainServer = - embeddedServer(Netty, host = host, port = port) { - configureWebconsole(this@startConsole, chatService) + // Start MCP server in background if enabled + if (McpServerHelper.isEnabled) { + runBlocking { + McpServerHelper.startMcpServer(8081) + + val translationResult = + this@startConsole.getTranslationResult()?.analysisResult?.translationResult + if (translationResult != null) { + McpServerHelper.setGlobalAnalysisResult(translationResult) } - mainServer.start(wait = true) + + // Initialize and connect custom MCP client + println("Initializing custom MCP client...") + customMcpClient = CustomMcpClient() + customMcpClient.connect() + println("Custom MCP client connected!") + } + } else { + println("MCP module not enabled, AI chat features will be disabled") } + + // Start main server on port 8080 + println("Starting main server on port $port...") + embeddedServer(Netty, host = host, port = port) { + configureWebconsole(this@startConsole, chatService, customMcpClient) + } + .start(wait = true) } /** @@ -73,7 +89,8 @@ fun ConsoleService.startConsole( */ fun Application.configureWebconsole( service: ConsoleService = ConsoleService(), - chatService: ChatService = ChatService(), + chatService: ChatService? = null, + customMcpClient: CustomMcpClient? = null, ) { install(CORS) { anyHost() @@ -89,7 +106,7 @@ fun Application.configureWebconsole( ) } - configureRouting(service, chatService) + configureRouting(service, chatService, customMcpClient) } /** @@ -98,11 +115,12 @@ fun Application.configureWebconsole( */ fun Application.configureRouting( service: ConsoleService, - chatService: ChatService = ChatService(), + chatService: ChatService? = null, + customMcpClient: CustomMcpClient? = null, ) { routing { // We'll add routes here - apiRoutes(service, chatService) + apiRoutes(service, chatService, customMcpClient) frontendRoutes() } } diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Nodes.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Nodes.kt index 6d7c8cc7240..76286c8a23e 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Nodes.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Nodes.kt @@ -36,10 +36,10 @@ import de.fraunhofer.aisec.cpg.assumptions.Assumption import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.concepts.Concept import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration -import de.fraunhofer.aisec.cpg.graph.edges.Edge import de.fraunhofer.aisec.cpg.passes.concepts.LoadPersistedConcepts import de.fraunhofer.aisec.cpg.passes.concepts.LoadPersistedConcepts.* import de.fraunhofer.aisec.cpg.query.* +import de.fraunhofer.aisec.cpg.serialization.* import io.github.detekt.sarif4k.ArtifactLocation import io.github.detekt.sarif4k.Result import java.net.URI @@ -47,14 +47,8 @@ import java.nio.file.Path import kotlin.io.path.Path import kotlin.io.path.toPath import kotlin.uuid.Uuid -import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.Transient -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder /** * JSON data class for an analysis request. It contains the source directory, an optional include @@ -68,14 +62,6 @@ data class AnalyzeRequestJSON( val conceptsFile: String? = null, ) -/** JSON data class for an [Edge]. */ -@Serializable -data class EdgeJSON( - var label: String, - @Serializable(with = UuidSerializer::class) var start: Uuid, - @Serializable(with = UuidSerializer::class) var end: Uuid, -) - /** JSON data class for a SARIF [Result]. */ @Serializable data class FindingsJSON( @@ -143,25 +129,6 @@ data class ConstructorArguments( val argumentType: String? = null, // currently not used ) -/** JSON data class for a [Node]. */ -@Serializable -data class NodeJSON( - @Serializable(with = UuidSerializer::class) val id: Uuid, - val type: String, - val startLine: Int, - val startColumn: Int, - val endLine: Int, - val endColumn: Int, - val code: String, - val name: String, - val astChildren: List, - val prevDFG: List = emptyList(), - val nextDFG: List = emptyList(), - @Serializable(with = UuidSerializer::class) val translationUnitId: Uuid? = null, - val componentName: String? = null, - val fileName: String? = null, -) - /** JSON data class for a requirement category. */ @Serializable data class RequirementsCategoryJSON( @@ -358,42 +325,6 @@ fun Component.toJSON(): ComponentJSON { ) } -/** Converts a [Node] into its JSON representation. */ -fun Node.toJSON(noEdges: Boolean = false): NodeJSON { - return NodeJSON( - id = this.id, - type = this.javaClass.simpleName, - startLine = location?.region?.startLine ?: -1, - startColumn = location?.region?.startColumn ?: -1, - endLine = location?.region?.endLine ?: -1, - endColumn = location?.region?.endColumn ?: -1, - code = this.code ?: "", - name = this.name.toString(), - fileName = - this.location?.artifactLocation?.uri?.let { uri -> - // Extract filename from URI - val path = uri.toString() - path.substringAfterLast('/').substringAfterLast('\\') - }, - astChildren = - if (noEdges) emptyList() - else (this as? AstNode)?.astChildren?.map { it.toJSON() } ?: emptyList(), - prevDFG = if (noEdges) emptyList() else this.prevDFGEdges.map { it.toJSON() }, - nextDFG = if (noEdges) emptyList() else this.nextDFGEdges.map { it.toJSON() }, - translationUnitId = this.translationUnit?.id, - componentName = this.component?.name?.toString(), - ) -} - -/** Converts an [Edge] into its JSON representation. */ -fun Edge<*>.toJSON(): EdgeJSON { - return EdgeJSON( - label = this.labels.firstOrNull() ?: "", - start = this.start.id, - end = this.end.id, - ) -} - /** Converts an [Assumption] into its JSON representation. */ fun Assumption.toJSON(): AssumptionJSON { return AssumptionJSON( @@ -462,23 +393,6 @@ val ArtifactLocation.absolutePath: Path? } } -/** - * Custom serializer for [Uuid] to convert it to and from a string representation. This is used for - * serialization and deserialization of [Uuid] in the JSON data classes. - */ -object UuidSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("Uuid", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: Uuid) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): Uuid { - return Uuid.parse(decoder.decodeString()) - } -} - /** Converts the requirement categories of an [AnalysisProject] into their JSON representation. */ fun AnalysisProject.requirementCategoriesToJSON( requirementsResults: Map>? = null diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt index 72be5e7526f..1bde7c4a755 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt @@ -25,6 +25,10 @@ */ package de.fraunhofer.aisec.codyze.console +import de.fraunhofer.aisec.codyze.console.ai.ChatRequestJSON +import de.fraunhofer.aisec.codyze.console.ai.ChatService +import de.fraunhofer.aisec.codyze.console.ai.CustomMcpClient +import de.fraunhofer.aisec.codyze.console.ai.McpServerHelper import de.fraunhofer.aisec.cpg.graph.concepts.Concept import de.fraunhofer.aisec.cpg.graph.listOverlayClasses import io.ktor.http.* @@ -59,7 +63,11 @@ import kotlin.reflect.KClass * - POST `/api/concept`: Adds a concept node to the current * [de.fraunhofer.aisec.codyze.AnalysisResult] */ -fun Routing.apiRoutes(service: ConsoleService, chatService: ChatService) { +fun Routing.apiRoutes( + service: ConsoleService, + chatService: ChatService?, + customMcpClient: CustomMcpClient? = null, +) { // The API routes are prefixed with /api route("/api") { // The endpoint to analyze a project @@ -245,17 +253,64 @@ fun Routing.apiRoutes(service: ConsoleService, chatService: ChatService) { } } - post("/chat") { - val request = call.receive() - call.respondTextWriter(contentType = ContentType.Text.EventStream) { - try { - chatService.chat(request).collect { chunk -> - chunk.split("\n").forEach { line -> write("data: $line\n") } - write("\n") // Empty line marks end of event - flush() + // Feature flags endpoint + get("/features") { call.respond(mapOf("mcpEnabled" to McpServerHelper.isEnabled)) } + + // Chat endpoint - only available if MCP module is enabled + if (chatService != null) { + post("/chat") { + val request = call.receive() + call.respondTextWriter(contentType = ContentType.Text.EventStream) { + try { + chatService.chat(request).collect { chunk -> + chunk.split("\n").forEach { line -> write("data: $line\n") } + write("\n") // Empty line marks end of event + flush() + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + // Custom MCP client endpoint - prototype with SSE + post("/chat-custom") { + if (customMcpClient == null) { + call.respondText( + "Custom MCP client not available", + status = HttpStatusCode.ServiceUnavailable, + ) + return@post + } + + val request = call.receive() + call.respondTextWriter(contentType = ContentType.Text.EventStream) { + try { + println("[Router] Starting custom MCP chat with reused client...") + customMcpClient.chat(request).collect { chunk -> + // Skip empty chunks to avoid client disconnection + if (chunk.isNotBlank()) { + // SSE format: each line must be prefixed with "data: " + // Split chunk by newlines and send each line separately + chunk.lines().forEach { line -> write("data: $line\n") } + write("\n") // Empty line marks end of event + flush() + } + } + println("[Router] Custom MCP chat completed") + } catch (e: Exception) { + println("[Router] Error in custom chat: ${e.message}") + e.printStackTrace() + // Only write error if channel is still open + try { + write("data: Error: ${e.message}\n\n") + flush() + } catch (writeException: Exception) { + println( + "[Router] Could not write error (channel closed): ${writeException.message}" + ) + } } - } catch (e: Exception) { - e.printStackTrace() } } } diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt new file mode 100644 index 00000000000..b40f8b4f327 --- /dev/null +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt @@ -0,0 +1,244 @@ +/* + * 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.console.ai + +import ai.koog.agents.core.agent.AIAgent +import ai.koog.agents.core.agent.GraphAIAgent.FeatureContext +import ai.koog.agents.core.dsl.builder.forwardTo +import ai.koog.agents.core.dsl.builder.strategy +import ai.koog.agents.core.dsl.extension.nodeExecuteMultipleTools +import ai.koog.agents.core.dsl.extension.nodeLLMRequestStreamingAndSendResults +import ai.koog.agents.core.dsl.extension.onMultipleToolCalls +import ai.koog.agents.core.environment.ReceivedToolResult +import ai.koog.agents.core.tools.ToolRegistry +import ai.koog.agents.features.eventHandler.feature.handleEvents +import ai.koog.agents.mcp.McpTool +import ai.koog.agents.mcp.McpToolRegistryProvider +import ai.koog.prompt.executor.clients.google.GoogleModels +import ai.koog.prompt.executor.llms.all.simpleGoogleAIExecutor +import ai.koog.prompt.executor.model.PromptExecutor +import ai.koog.prompt.message.Message +import ai.koog.prompt.message.RequestMetaInfo +import ai.koog.prompt.streaming.StreamFrame +import com.typesafe.config.ConfigFactory +import de.fraunhofer.aisec.cpg.graph.invoke +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.serialization.Serializable + +@Serializable data class ChatMessageJSON(val role: String, val content: String) + +@Serializable data class ChatRequestJSON(val messages: List) + +class ChatService() { + private val config = ConfigFactory.load() + private val provider: String = config.getString("llm.provider") + private val mcpServerUrl: String = config.getString("mcp.serverUrl") + + fun chat(request: ChatRequestJSON): Flow = channelFlow { + val userMessage = request.messages.lastOrNull()?.content ?: "" + simpleGoogleAIExecutor("REDACTED_API_KEY").use { executor -> + // Create tool registry + val transport = McpToolRegistryProvider.defaultSseTransport(mcpServerUrl) + val toolRegistry = + McpToolRegistryProvider.fromTransport( + transport = transport, + name = "codyze-console", + version = "1.0.0", + ) + + // Simple agent with event handlers to capture tool results + val agent = + AIAgent( + promptExecutor = executor, + llmModel = GoogleModels.Gemini2_5Pro, + toolRegistry = toolRegistry, + systemPrompt = "You are a code analysis assistant with access to CPG tools.", + temperature = 0.0, + installFeatures = { + handleEvents { + onToolCallCompleted { event -> + println("[DEBUG] šŸ”§ Tool completed: ${event.tool.name}") + try { + // Extract JSON strings from TextContent items + val jsonItems = + (event.result as? McpTool.Result) + ?.promptMessageContents + ?.map { + (it + as? + io.modelcontextprotocol.kotlin.sdk.TextContent) + ?.text + } + + // Create JSON array string + val jsonArray = "[${jsonItems?.joinToString(",")}]" + + println( + "[DEBUG] Tool result JSON array with ${jsonItems?.size} items" + ) + + // Send it wrapped in the markers the frontend expects + send("__TOOL_RESULT_START__") + send(jsonArray) + send("__TOOL_RESULT_END__") + } catch (e: Exception) { + println("[ERROR] Failed to send tool result: ${e.message}") + e.printStackTrace() + } + } + + onLLMStreamingFrameReceived { context -> + // Stream LLM responses + (context.streamFrame as? StreamFrame.Append)?.let { frame -> + print(frame.text) + } + } + } + }, + ) + + // Run the agent and get the result + println("[DEBUG] Running agent...") + val result = agent.run(userMessage) + println("[DEBUG] Agent result: $result") + + // Send the final result if not empty + if (result.isNotEmpty()) { + send(result) + } + send("[DONE]") + } + } +} + +private fun createAgent( + toolRegistry: ToolRegistry, + executor: PromptExecutor, + installFeatures: FeatureContext.() -> Unit = {}, +) = + AIAgent( + promptExecutor = executor, + strategy = streamingWithToolsStrategy(), + llmModel = GoogleModels.Gemini2_5Pro, + systemPrompt = + """ + You are a helpful assistant for code analysis using the Code Property Graph (CPG). + You have access to various CPG analysis tools through MCP. + Use these tools proactively to analyze code and answer questions about code structure. + + When multiple independent tools are needed, call them in a single turn if possible. + """ + .trimIndent(), + temperature = 0.0, + toolRegistry = toolRegistry, + installFeatures = installFeatures, + ) + +// private fun getLLMModel(): LLModel { +// val model = +// when (provider.lowercase()) { +// "gemini" -> config.getString("llm.gemini.model") +// "ollama" -> config.getString("llm.ollama.model") +// "vLLM" -> config.getString("llm.vLLM.model") +// else -> throw IllegalArgumentException("Unsupported LLM provider: $provider") +// } +// +// return when (provider.lowercase()) { +// "gemini" -> +// LLModel( +// provider = LLMProvider.Google, +// id = GoogleModels.Gemini2_5Pro.id, +// capabilities = listOf(LLMCapability.Temperature, LLMCapability.Tools), +// contextLength = 128000, +// maxOutputTokens = 8192, +// ) +// +// "ollama" -> +// LLModel( +// provider = LLMProvider.Ollama, +// id = model, +// capabilities = listOf(LLMCapability.Tools), +// contextLength = 65536, +// maxOutputTokens = 8192, +// ) +// +// "vLLM" -> +// LLModel( +// provider = LLMProvider.OpenAI, +// id = model, +// capabilities = listOf(LLMCapability.Tools), +// contextLength = 65536, +// maxOutputTokens = 8192, +// ) +// +// else -> throw IllegalArgumentException("Unsupported LLM provider: $provider") +// } +// } + +fun streamingWithToolsStrategy() = + strategy("streaming_loop") { + val executeMultipleTools by nodeExecuteMultipleTools(parallelTools = true) + val nodeStreaming by nodeLLMRequestStreamingAndSendResults() + + val mapStringToRequests by + node> { input -> + listOf(Message.User(content = input, metaInfo = RequestMetaInfo.Empty)) + } + + val applyRequestToSession by + node, List> { input -> + llm.writeSession { + appendPrompt { + input.filterIsInstance().forEach { user(it.content) } + + tool { + input.filterIsInstance().forEach { result(it) } + } + } + input + } + } + + val mapToolCallsToRequests by + node, List> { input -> + input.map { it.toMessage() } + } + + edge(nodeStart forwardTo mapStringToRequests) + edge(mapStringToRequests forwardTo applyRequestToSession) + edge(applyRequestToSession forwardTo nodeStreaming) + edge(nodeStreaming forwardTo executeMultipleTools onMultipleToolCalls { true }) + edge(executeMultipleTools forwardTo mapToolCallsToRequests) + edge(mapToolCallsToRequests forwardTo applyRequestToSession) + edge( + nodeStreaming forwardTo + nodeFinish onCondition + { + it.filterIsInstance().isEmpty() + } + ) + } diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/CustomMcpClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/CustomMcpClient.kt new file mode 100644 index 00000000000..ce4fdc77a4e --- /dev/null +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/CustomMcpClient.kt @@ -0,0 +1,466 @@ +/* + * 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.console.ai + +import com.typesafe.config.ConfigFactory +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* +import io.ktor.client.plugins.sse.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.utils.io.* +import io.modelcontextprotocol.kotlin.sdk.client.Client +import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions +import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport +import io.modelcontextprotocol.kotlin.sdk.types.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.serialization.json.* + +/** + * Custom MCP client that connects to the local MCP server and uses Ollama for LLM interactions. + * This is a prototype implementation for testing. + */ +class CustomMcpClient : AutoCloseable { + private val config = ConfigFactory.load() + private val mcpServerUrl: String = config.getString("mcp.serverUrl") + private val ollamaBaseUrl: String = config.getString("llm.ollama.baseUrl") + private val ollamaModel: String = config.getString("llm.ollama.model") + + private val httpClient = + HttpClient(CIO) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + isLenient = true + } + ) + } + // Install SSE plugin for MCP client transport + install(SSE) + + // Configure timeouts - critical for long-running operations! + install(HttpTimeout) { + requestTimeoutMillis = 600_000 // 10 minutes for long tool executions + connectTimeoutMillis = 30_000 // 30 seconds for connection + socketTimeoutMillis = 600_000 // 10 minutes for socket operations + } + + // Optional: Enable logging for debugging + install(Logging) { + level = LogLevel.INFO + logger = + object : Logger { + override fun log(message: String) { + println("[HTTP] $message") + } + } + } + } + + private val mcp: Client = + Client( + clientInfo = Implementation(name = "codyze-custom-client", version = "1.0.0"), + options = + ClientOptions(capabilities = ClientCapabilities(sampling = buildJsonObject {})), + ) + + private var tools: List = emptyList() + + /** Connect to the MCP server via SSE */ + suspend fun connect() { + println("[CustomMCP] Connecting to MCP server at $mcpServerUrl...") + + // Create SSE transport - requires HttpClient + val transport = SseClientTransport(urlString = mcpServerUrl, client = httpClient) + + // Register sampling request handler BEFORE connecting + registerSamplingHandler() + + mcp.connect(transport) + + // List available tools + val toolsResult = mcp.listTools() + tools = toolsResult.tools + + println("[CustomMCP] Connected! Available tools: ${tools.map { it.name }}") + } + + /** Register handler for incoming sampling requests from the server */ + private fun registerSamplingHandler() { + println("[CustomMCP] Registering sampling request handler...") + + mcp.setRequestHandler(Method.Defined.SamplingCreateMessage) { + request, + _ -> + try { + // Send to Ollama (non-streaming) + val llmResponse = + sendToOllamaNonStreaming( + messages = request.messages, + systemPrompt = request.systemPrompt, + maxTokens = request.maxTokens, + ) + + println("[CustomMCP] LLM response received: ${llmResponse.take(100)}...") + + // Return result to server + CreateMessageResult( + role = Role.Assistant, + content = TextContent(text = llmResponse), + model = ollamaModel, + stopReason = StopReason.EndTurn, + ) + } catch (e: Exception) { + println("[CustomMCP] Error in sampling handler: ${e.message}") + e.printStackTrace() + + CreateMessageResult( + role = Role.Assistant, + content = TextContent(text = "Error: ${e.message}"), + model = ollamaModel, + stopReason = StopReason.EndTurn, + ) + } + } + } + + /** Send messages to Ollama without streaming (for sampling) */ + private suspend fun sendToOllamaNonStreaming( + messages: List, + systemPrompt: String?, + maxTokens: Int, + ): String { + println("[CustomMCP] Sending to Ollama (non-streaming)...") + + // Convert MCP messages to Ollama format + val ollamaMessages = buildJsonArray { + // Add system prompt if provided + if (systemPrompt != null) { + add( + buildJsonObject { + put("role", "system") + put("content", systemPrompt) + } + ) + } + + // Add conversation messages + messages.forEach { msg -> + add( + buildJsonObject { + put("role", msg.role.toString().lowercase()) + val content = (msg.content as? TextContent)?.text ?: "" + put("content", content) + } + ) + } + } + + // Call Ollama API + val response = + httpClient.post("$ollamaBaseUrl/v1/chat/completions") { + contentType(ContentType.Application.Json) + setBody( + buildJsonObject { + put("model", ollamaModel) + put("messages", ollamaMessages) + put("max_tokens", maxTokens) + put("stream", false) // Important: NO streaming for sampling + } + ) + } + + val result = response.body() + + // Extract response text + val choices = result["choices"]?.jsonArray + val firstChoice = choices?.firstOrNull()?.jsonObject + val message = firstChoice?.get("message")?.jsonObject + val content = message?.get("content")?.jsonPrimitive?.content + + return content ?: "No response from Ollama" + } + + /** Process a chat query using Ollama with MCP tool support */ + fun chat(request: ChatRequestJSON): Flow = channelFlow { + println("[CustomMCP] Processing query...") + + val userMessage = request.messages.lastOrNull()?.content ?: "" + println("[CustomMCP] User message: $userMessage") + + // Convert MCP tools to Ollama format + val ollamaTools = + tools.map { tool -> + buildJsonObject { + put("type", "function") + put( + "function", + buildJsonObject { + put("name", tool.name) + put("description", tool.description ?: "") + put("parameters", tool.inputSchema.properties ?: buildJsonObject {}) + }, + ) + } + } + + println("[CustomMCP] Converted ${ollamaTools.size} tools for Ollama") + + // Build messages for Ollama + val messages = buildJsonArray { + add( + buildJsonObject { + put("role", "system") + put("content", "You are a code analysis assistant with access to CPG tools.") + } + ) + add( + buildJsonObject { + put("role", "user") + put("content", userMessage) + } + ) + } + + // Call Ollama with streaming + println( + "[CustomMCP] Calling Ollama at $ollamaBaseUrl with model $ollamaModel (streaming)..." + ) + + try { + val accumulatedToolCalls = mutableListOf() + + httpClient + .preparePost("$ollamaBaseUrl/v1/chat/completions") { + contentType(ContentType.Application.Json) + setBody( + buildJsonObject { + put("model", ollamaModel) + put("messages", messages) + put("tools", JsonArray(ollamaTools)) + put("stream", true) + } + ) + } + .execute { response -> + val channel = response.body() + + println("[CustomMCP] Receiving streaming response...") + + try { + while (!channel.isClosedForRead) { + val line = + try { + channel.readUTF8Line() + } catch (e: Exception) { + println("[CustomMCP] Error reading line: ${e.message}") + break + } + + // If line is null, check if channel is closed + if (line == null) { + if (channel.isClosedForRead) { + println("[CustomMCP] Channel closed, ending stream") + break + } + // Otherwise, continue reading (might be temporary buffer issue) + continue + } + + if (line.isBlank()) continue + if (line.startsWith("data: ")) { + val jsonStr = line.substringAfter("data: ").trim() + if (jsonStr == "[DONE]") { + println("[CustomMCP] Stream completed") + break + } + + try { + val chunk = Json.parseToJsonElement(jsonStr).jsonObject + val choices = chunk["choices"]?.jsonArray + val firstChoice = choices?.firstOrNull()?.jsonObject + val delta = firstChoice?.get("delta")?.jsonObject + + // Stream content tokens + val content = + delta?.get("content")?.jsonPrimitive?.contentOrNull + if (content != null) { + // Send as JSON-wrapped event + val jsonEvent = buildJsonObject { + put("type", "text") + put("content", content) + } + send(Json.encodeToString(jsonEvent)) + } + + // Collect tool calls from delta + val toolCalls = delta?.get("tool_calls")?.jsonArray + if (toolCalls != null && toolCalls.isNotEmpty()) { + println( + "[CustomMCP] ========== TOOL CALL DETECTED ==========" + ) + println("[CustomMCP] Full chunk: $chunk") + println("[CustomMCP] Tool calls: $toolCalls") + println( + "[CustomMCP] ========================================" + ) + toolCalls.forEach { + accumulatedToolCalls.add(it.jsonObject) + } + } + } catch (e: Exception) { + println("[CustomMCP] Error parsing chunk: ${e.message}") + } + } + } + } catch (e: Exception) { + println("[CustomMCP] Stream reading error: ${e.message}") + e.printStackTrace() + // Don't throw - just break the loop and continue with tool calls + } + } + + // Now handle the accumulated tool calls + if (accumulatedToolCalls.isNotEmpty()) { + println( + "[CustomMCP] Processing ${accumulatedToolCalls.size} accumulated tool calls..." + ) + + for (toolCallObj in accumulatedToolCalls) { + val function = toolCallObj["function"]?.jsonObject + val toolName = function?.get("name")?.jsonPrimitive?.contentOrNull + val argumentsStr = + function?.get("arguments")?.jsonPrimitive?.contentOrNull ?: "{}" + + if (toolName != null) { + println("[CustomMCP] Calling MCP tool: $toolName") + + try { + // Parse arguments as JsonObject and convert to Map + val jsonArgs = Json.parseToJsonElement(argumentsStr).jsonObject + val arguments: Map = jsonArgs.toMap() + + // Call MCP tool + val result = mcp.callTool(name = toolName, arguments = arguments) + + // Extract and send result + val resultText = + result?.content?.joinToString("\n") { + (it as? TextContent)?.text ?: "" + } ?: "No result" + + println("[CustomMCP] Tool result length: ${resultText.length} chars") + println("[CustomMCP] Tool result preview: ${resultText.take(500)}...") + + // Parse newline-separated JSON objects into an array + val resultArray = + try { + val items = + resultText + .trim() + .split("\n") + .filter { it.isNotBlank() } + .map { Json.parseToJsonElement(it) } + JsonArray(items) + } catch (e: Exception) { + println( + "[CustomMCP] Warning: Could not parse as newline-separated JSON, treating as single item" + ) + // Fallback: try to parse as single JSON + JsonArray(listOf(Json.parseToJsonElement(resultText))) + } + + // Send tool result with parsed array + val jsonEvent = buildJsonObject { + put("type", "tool_result") + put("tool", toolName) + put("data", resultArray) + } + send(Json.encodeToString(jsonEvent)) + } catch (e: Exception) { + println("[CustomMCP] Tool call failed: ${e.message}") + e.printStackTrace() + val errorEvent = buildJsonObject { + put("type", "text") + put("content", "āŒ **Tool failed:** ${e.message}\n\n") + } + send(Json.encodeToString(errorEvent)) + } + } + } + } + } catch (e: Exception) { + println("[CustomMCP] Error: ${e.message}") + e.printStackTrace() + val errorEvent = buildJsonObject { + put("type", "text") + put("content", "Error: ${e.message}") + } + send(Json.encodeToString(errorEvent)) + } + } + + override fun close() { + httpClient.close() + } +} + +// Helper to convert JsonObject to Map +private fun JsonObject.toMap(): Map { + return this.mapValues { (_, value) -> + when (value) { + is JsonPrimitive -> { + when { + value.isString -> value.content + value.booleanOrNull != null -> value.boolean + value.intOrNull != null -> value.int + value.longOrNull != null -> value.long + value.doubleOrNull != null -> value.double + else -> value.content + } + } + + is JsonObject -> value.toMap() + is JsonArray -> + value.map { + when (it) { + is JsonPrimitive -> it.content + is JsonObject -> it.toMap() + else -> it.toString() + } + } + + else -> value.toString() + } + } +} diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpServerHelper.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpServerHelper.kt new file mode 100644 index 00000000000..7128abbe614 --- /dev/null +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpServerHelper.kt @@ -0,0 +1,71 @@ +/* + * 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.console.ai + +/** + * Helper object to make the features of the `cpg-mcp` module conditionally available. When the + * module is available in the build, this will use the actual MCP functions directly. + */ +object McpServerHelper { + /** Check if mcp module is enabled */ + val isEnabled: Boolean by lazy { + try { + Class.forName("de.fraunhofer.aisec.cpg.mcp.ApplicationKt") + true + } catch (_: ClassNotFoundException) { + false + } + } + + suspend fun startMcpServer(port: Int) { + if (!isEnabled) { + return + } + + try { + println("Starting MCP server on port $port...") + val server = de.fraunhofer.aisec.cpg.mcp.mcpserver.configureServer() + de.fraunhofer.aisec.cpg.mcp.runSseMcpServerUsingKtorPlugin(port, server) + } catch (e: Exception) { + println("Failed to start MCP server: ${e.message}") + e.printStackTrace() + } + } + + /** Set the global analysis result in the `cpg-mcp` module */ + fun setGlobalAnalysisResult(result: de.fraunhofer.aisec.cpg.TranslationResult) { + if (!isEnabled) { + return + } + + try { + de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.globalAnalysisResult = result + } catch (e: Exception) { + println("Warning: Failed to set globalAnalysisResult in cpg-mcp module: ${e.message}") + e.printStackTrace() + } + } +} diff --git a/codyze-console/src/main/resources/application.conf b/codyze-console/src/main/resources/application.conf index afd87b89437..f4c78e8d5b9 100644 --- a/codyze-console/src/main/resources/application.conf +++ b/codyze-console/src/main/resources/application.conf @@ -1,5 +1,5 @@ llm { - provider = "ollama" # Options: "ollama", "gemini", vLLM + provider = "gemini" # Options: "ollama", "gemini", vLLM gemini { model = "gemini-2.0-flash-exp" @@ -18,5 +18,5 @@ llm { mcp { # MCP Server Configuration - serverUrl = "http://localhost:8081/sse" + serverUrl = "http://localhost:8081" } \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/ChatInterface.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte similarity index 54% rename from codyze-console/src/main/webapp/src/lib/components/ai-assistant/ChatInterface.svelte rename to codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte index 424d08b33ed..47b1b09aac5 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/ChatInterface.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte @@ -1,14 +1,70 @@ -
- -
+
+ +
+ +
{:else} - +
-
- -
+ {#if message.contentType === 'tool-result' && message.toolResults && message.toolResults.length > 0} + + + {:else if message.content} + +
+ +
+ {/if}
{/if} {/each} @@ -122,9 +192,7 @@
{#if displayContent}
-
- {displayContent} -
+
{:else} @@ -159,4 +227,71 @@ />
+
+ + + {#if showCodePreview && selectedNode} +
+ +
+ {/if}
+ + diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/CodePreview.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/CodePreview.svelte new file mode 100644 index 00000000000..f856fb36423 --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/CodePreview.svelte @@ -0,0 +1,212 @@ + + +
+ +
+
+ + + +
+
{node.name}
+
+ {fileName}:{node.startLine} +
+
+
+ +
+ + +
+ +
+ + + +
+ + diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/MarkdownRenderer.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/MarkdownRenderer.svelte similarity index 100% rename from codyze-console/src/main/webapp/src/lib/components/ai-assistant/MarkdownRenderer.svelte rename to codyze-console/src/main/webapp/src/lib/components/ai-agent/MarkdownRenderer.svelte diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/MessageInput.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/MessageInput.svelte similarity index 100% rename from codyze-console/src/main/webapp/src/lib/components/ai-assistant/MessageInput.svelte rename to codyze-console/src/main/webapp/src/lib/components/ai-agent/MessageInput.svelte diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ResultsList.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ResultsList.svelte new file mode 100644 index 00000000000..e2cff4c2b14 --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ResultsList.svelte @@ -0,0 +1,203 @@ + + +
+ +
+ {items.length} result{items.length !== 1 ? 's' : ''} +
+ + +
+ {#each items as item, idx} + + {/each} +
+
+ + \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/WelcomeScreen.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/WelcomeScreen.svelte similarity index 99% rename from codyze-console/src/main/webapp/src/lib/components/ai-assistant/WelcomeScreen.svelte rename to codyze-console/src/main/webapp/src/lib/components/ai-agent/WelcomeScreen.svelte index 8ea3a79a4e6..c33d46bb97d 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-assistant/WelcomeScreen.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/WelcomeScreen.svelte @@ -33,7 +33,7 @@

Hi, I'm your CodAIze AssistantCodAIze Agent
(null); let codeContainerElement = $state(); + // Collapsible panel state + let isPanelCollapsed = $state(false); + + function togglePanel() { + isPanelCollapsed = !isPanelCollapsed; + } + const tabs = $derived([ { id: 'overlayNodes', @@ -80,6 +87,29 @@
{translationUnit.name}
+ + +
@@ -108,19 +138,70 @@
- -
-
- -
+ + {#if !isPanelCollapsed} +
+
+ +
-
- console.log('Node clicked:', node)} - /> +
+ console.log('Node clicked:', node)} + /> +
-
-
\ No newline at end of file + {/if} +
+ + \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte b/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte index 26ea46ed841..00d4fa5a23c 100644 --- a/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte @@ -1,5 +1,6 @@ @@ -49,49 +65,69 @@ diff --git a/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts b/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts index d81da38e983..4937b58298e 100644 --- a/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts +++ b/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts @@ -30,6 +30,38 @@ class LLMAgent { callbacks.onComplete ); } + + // Custom MCP client endpoint - prototype + async chatCustom(messages: LLMMessage[], callbacks: StreamingCallbacks): Promise { + console.log('[LLMAgent] Using custom MCP client endpoint'); + console.log('[LLMAgent] Messages:', messages); + + const requestData = { + messages: messages.map((msg) => ({ + role: msg.role, + content: msg.content + })) + }; + + console.log('[LLMAgent] Request data:', requestData); + + await this.apiService.streamPost( + '/api/chat-custom', + requestData, + (chunk) => { + console.log('[LLMAgent] Received chunk:', chunk); + callbacks.onChunk(chunk); + }, + (error) => { + console.error('[LLMAgent] Error:', error); + callbacks.onError?.(error); + }, + () => { + console.log('[LLMAgent] Stream complete'); + callbacks.onComplete?.(); + } + ); + } } export const llmAgent = new LLMAgent(); diff --git a/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.svelte b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte similarity index 60% rename from codyze-console/src/main/webapp/src/routes/ai-assistant/+page.svelte rename to codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte index 441668c14bd..bdaa030e535 100644 --- a/codyze-console/src/main/webapp/src/routes/ai-assistant/+page.svelte +++ b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte @@ -1,6 +1,7 @@ + +
+ +
+
+ + + + LLM Analysis +
+ {suggestions.length} suggestion{suggestions.length !== 1 ? 's' : ''} +
+ + +
+ {#if summary} +
+
Summary
+

{summary}

+
+ {/if} + + {#if suggestions.length > 0} + + {#if conceptSuggestions.length > 0} +
+
+ + + + Concepts ({conceptSuggestions.length}) +
+
+ {#each conceptSuggestions as suggestion} +
+ + + {#if expandedSuggestions.has(suggestion.nodeId)} +
+ {#if suggestion.reasoning} +
+ Reasoning: +

{suggestion.reasoning}

+
+ {/if} + {#if suggestion.securityImpact} +
+ Security Impact: +

{suggestion.securityImpact}

+
+ {/if} + +
+ {/if} +
+ {/each} +
+
+ {/if} + + + {#if operationSuggestions.length > 0} +
+
+ + + + Operations ({operationSuggestions.length}) +
+
+ {#each operationSuggestions as suggestion} +
+ + + {#if expandedSuggestions.has(suggestion.nodeId)} +
+ {#if suggestion.reasoning} +
+ Reasoning: +

{suggestion.reasoning}

+
+ {/if} + {#if suggestion.securityImpact} +
+ Security Impact: +

{suggestion.securityImpact}

+
+ {/if} + +
+ {/if} +
+ {/each} +
+
+ {/if} + + + + {:else} +
+ + + +

No suggestions generated

+
+ {/if} +
+
+ + \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/NodeListWidget.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/NodeListWidget.svelte new file mode 100644 index 00000000000..4597b79f61b --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/NodeListWidget.svelte @@ -0,0 +1,241 @@ + + +
+ +
+
+ + + + {toolDisplayName} +
+ {items.length} result{items.length !== 1 ? 's' : ''} +
+ + +
+ {#each items as item, idx} + + {/each} +
+
+ + \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte new file mode 100644 index 00000000000..5a6246016de --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte @@ -0,0 +1,139 @@ + + +
+ {#if selectedWidget} + + {@const SelectedWidget = selectedWidget} + + {:else} + +
+
+ {#if data.toolName} + {data.toolName} + {/if} + {#if data.isError} + Error + {/if} +
+
{renderFallback(data.content)}
+
+ {/if} +
+ + \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts new file mode 100644 index 00000000000..6a8e31328c1 --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts @@ -0,0 +1,46 @@ +import type { Component } from 'svelte'; + +export type ToolResultData = { + toolName?: string; + content: any; + isError?: boolean; +}; + +export type WidgetProps = { + data: ToolResultData; + onItemClick?: (item: any) => void; +}; + +export type WidgetComponent = Component; + +type WidgetMatcher = (data: ToolResultData) => boolean; + +interface WidgetRegistration { + component: WidgetComponent; + matcher: WidgetMatcher; +} + +class WidgetRegistry { + private registrations: WidgetRegistration[] = []; + + register(component: WidgetComponent, matcher: WidgetMatcher) { + this.registrations.push({ component, matcher }); + } + + /** + * Find the appropriate widget component for the given data. + * Returns null if no matching widget is found. + */ + getWidget(data: ToolResultData): WidgetComponent | null { + for (let i = 0; i < this.registrations.length; i++) { + const registration = this.registrations[i]; + const matches = registration.matcher(data); + if (matches) { + return registration.component; + } + } + return null; + } +} + +export const widgetRegistry = new WidgetRegistry(); \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte b/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte index 00d4fa5a23c..d2b26bc9990 100644 --- a/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/navigation/Sidebar.svelte @@ -70,62 +70,58 @@ class="flex cursor-not-allowed items-center rounded-md px-3 py-2 text-sm font-medium text-gray-400" title="MCP module not enabled" > - - ({#if item.icon === 'robot'} - ) - {/if} - + {#if item.icon === 'robot'} + + + + {/if} {item.name} {:else} - - {#if item.icon === 'home'} - - {:else if item.icon === 'clipboard-check'} - - {:else if item.icon === 'code'} - - {:else if item.icon === 'robot'} - - {/if} - - {item.name} + {#if item.icon === 'robot'} + + + + {:else} + + {#if item.icon === 'home'} + + {:else if item.icon === 'clipboard-check'} + + {:else if item.icon === 'code'} + + {/if} + + {/if} + {item.name} {/if} diff --git a/codyze-console/src/main/webapp/src/lib/services/apiService.ts b/codyze-console/src/main/webapp/src/lib/services/apiService.ts index 383352a5362..ddd4633abbb 100644 --- a/codyze-console/src/main/webapp/src/lib/services/apiService.ts +++ b/codyze-console/src/main/webapp/src/lib/services/apiService.ts @@ -1,56 +1,6 @@ -interface ApiResponse { - ok: boolean; - status: number; - statusText: string; - data?: T; - error?: string; -} - class ApiService { constructor(private readonly baseUrl: string = '') {} - private async request(url: string, options: RequestInit): Promise> { - try { - const response = await fetch(`${this.baseUrl}${url}`, { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - ...(options.headers ?? {}) - }, - ...options - }); - - const data = response.ok ? await response.json() : undefined; - - return { - ok: response.ok, - status: response.status, - statusText: response.statusText, - data, - error: response.ok ? undefined : `HTTP ${response.status}: ${response.statusText}` - }; - } catch (err) { - return { - ok: false, - status: 0, - statusText: 'Network Error', - error: err instanceof Error ? err.message : String(err) - }; - } - } - - get(url: string, headers: Record = {}) { - return this.request(url, { method: 'GET', headers }); - } - - post(url: string, body: any, headers: Record = {}) { - return this.request(url, { - method: 'POST', - headers, - body: JSON.stringify(body) - }); - } - async streamPost( url: string, body: any, diff --git a/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts b/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts index 4937b58298e..0e5c56f5a21 100644 --- a/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts +++ b/codyze-console/src/main/webapp/src/lib/services/llmAgent.ts @@ -1,9 +1,5 @@ import ApiService from './apiService'; - -export interface LLMMessage { - role: 'user' | 'assistant' | 'system'; - content: string; -} +import type { LLMMessage } from '$lib/types'; export interface StreamingCallbacks { onChunk: (chunk: string) => void; @@ -30,38 +26,6 @@ class LLMAgent { callbacks.onComplete ); } - - // Custom MCP client endpoint - prototype - async chatCustom(messages: LLMMessage[], callbacks: StreamingCallbacks): Promise { - console.log('[LLMAgent] Using custom MCP client endpoint'); - console.log('[LLMAgent] Messages:', messages); - - const requestData = { - messages: messages.map((msg) => ({ - role: msg.role, - content: msg.content - })) - }; - - console.log('[LLMAgent] Request data:', requestData); - - await this.apiService.streamPost( - '/api/chat-custom', - requestData, - (chunk) => { - console.log('[LLMAgent] Received chunk:', chunk); - callbacks.onChunk(chunk); - }, - (error) => { - console.error('[LLMAgent] Error:', error); - callbacks.onError?.(error); - }, - () => { - console.log('[LLMAgent] Stream complete'); - callbacks.onComplete?.(); - } - ); - } } export const llmAgent = new LLMAgent(); diff --git a/codyze-console/src/main/webapp/src/lib/types.ts b/codyze-console/src/main/webapp/src/lib/types.ts index 24e8dc83fc3..9a7945f63bb 100644 --- a/codyze-console/src/main/webapp/src/lib/types.ts +++ b/codyze-console/src/main/webapp/src/lib/types.ts @@ -69,6 +69,27 @@ export interface EdgeJSON { end: string; } +// AI Agent / Chat interfaces +export interface LLMMessage { + role: 'user' | 'assistant' | 'system'; + content: string; +} + +export interface ToolResult { + toolName?: string; + content: any; + isError?: boolean; +} + +export interface ChatMessage { + id: string; + role: 'user' | 'assistant'; + content: string; + contentType?: 'text' | 'tool-result'; + toolResult?: ToolResult; + timestamp: Date; +} + export interface ConceptInfo { conceptName: string; constructorInfo: ConstructorInfo[]; diff --git a/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte index bdaa030e535..e85fa24ff8c 100644 --- a/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte +++ b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte @@ -1,11 +1,10 @@ - -
- -
- {items.length} result{items.length !== 1 ? 's' : ''} -
- - -
- {#each items as item, idx} - - {/each} -
-
- - \ No newline at end of file From 947863fb8ea3533a1804193a48effd5fc133296e Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 5 Jan 2026 17:11:00 +0100 Subject: [PATCH 44/93] Update imports of kotlin mcp sdk in cpg-mcp module --- .../de/fraunhofer/aisec/codyze/console/ai/ChatService.kt | 6 +----- .../de/fraunhofer/aisec/codyze/console/ai/McpClient.kt | 2 +- .../fraunhofer/aisec/codyze/console/ai/McpServerHelper.kt | 2 +- .../webapp/src/lib/components/ai-agent/ChatInterface.svelte | 3 --- .../src/main/webapp/src/lib/components/ai-agent/index.ts | 1 - cpg-mcp/src/integrationTest/kotlin/mcp/ApplyConceptsTest.kt | 4 ++-- .../src/integrationTest/kotlin/mcp/CpgAnalyzeToolTest.kt | 5 ++++- .../src/integrationTest/kotlin/mcp/CpgLlmAnalyzeToolTest.kt | 5 ++++- cpg-mcp/src/integrationTest/kotlin/mcp/ListCommandsTest.kt | 4 ++-- .../aisec/cpg/mcp/mcpserver/tools/CpgAnalyzeTool.kt | 2 +- .../aisec/cpg/mcp/mcpserver/tools/CpgDataflowTool.kt | 4 ++-- .../fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt | 4 ++-- 12 files changed, 20 insertions(+), 22 deletions(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt index 31f095d309a..9b1ff8bc8c3 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt @@ -39,10 +39,6 @@ import kotlinx.serialization.Serializable class ChatService(private val mcpClient: McpClient? = null) { fun chat(request: ChatRequestJSON): Flow { - return if (mcpClient != null) { - mcpClient.chat(request) - } else { - throw IllegalStateException("McpClient not initialized") - } + return mcpClient?.chat(request) ?: throw IllegalStateException("McpClient not initialized") } } diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpClient.kt index 359c961ebad..e0ae4562c50 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpClient.kt @@ -75,7 +75,7 @@ class McpClient : AutoCloseable { private val mcp: Client = Client( - clientInfo = Implementation(name = "codyze-custom-client", version = "1.0.0"), + clientInfo = Implementation(name = "codyze-client", version = "1.0.0"), options = ClientOptions(capabilities = ClientCapabilities(sampling = buildJsonObject {})), ) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpServerHelper.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpServerHelper.kt index 7128abbe614..53bdc572954 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpServerHelper.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpServerHelper.kt @@ -40,7 +40,7 @@ object McpServerHelper { } } - suspend fun startMcpServer(port: Int) { + fun startMcpServer(port: Int) { if (!isEnabled) { return } diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte index 38ec0e9b8df..1ac0ad79278 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte @@ -6,9 +6,6 @@ import ApiService from '$lib/services/apiService'; import type { NodeJSON, AnalysisResultJSON, TranslationUnitJSON, ChatMessage } from '$lib/types'; - const apiService = new ApiService(); - - // State for code preview split-view let selectedNode = $state(null); let showCodePreview = $derived(selectedNode !== null); diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/index.ts b/codyze-console/src/main/webapp/src/lib/components/ai-agent/index.ts index 0e1047a29e0..30dfb9ddaf1 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/index.ts +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/index.ts @@ -2,5 +2,4 @@ export { default as WelcomeScreen } from './WelcomeScreen.svelte'; export { default as ChatInterface } from './ChatInterface.svelte'; export { default as MarkdownRenderer } from './MarkdownRenderer.svelte'; export { default as MessageInput } from './MessageInput.svelte'; -export { default as ResultsList } from './ResultsList.svelte'; export { default as CodePreview } from './CodePreview.svelte'; diff --git a/cpg-mcp/src/integrationTest/kotlin/mcp/ApplyConceptsTest.kt b/cpg-mcp/src/integrationTest/kotlin/mcp/ApplyConceptsTest.kt index 0b297ac42ad..288fc639ca6 100644 --- a/cpg-mcp/src/integrationTest/kotlin/mcp/ApplyConceptsTest.kt +++ b/cpg-mcp/src/integrationTest/kotlin/mcp/ApplyConceptsTest.kt @@ -32,12 +32,12 @@ import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.globalAnalysisResult import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.listConceptsAndOperations import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.runCpgAnalyze import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalyzePayload -import io.modelcontextprotocol.kotlin.sdk.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.Implementation import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull diff --git a/cpg-mcp/src/integrationTest/kotlin/mcp/CpgAnalyzeToolTest.kt b/cpg-mcp/src/integrationTest/kotlin/mcp/CpgAnalyzeToolTest.kt index 5303685d638..a912436942d 100644 --- a/cpg-mcp/src/integrationTest/kotlin/mcp/CpgAnalyzeToolTest.kt +++ b/cpg-mcp/src/integrationTest/kotlin/mcp/CpgAnalyzeToolTest.kt @@ -30,9 +30,12 @@ import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.globalAnalysisResult import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.runCpgAnalyze import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalysisResult import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalyzePayload -import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.Implementation +import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs diff --git a/cpg-mcp/src/integrationTest/kotlin/mcp/CpgLlmAnalyzeToolTest.kt b/cpg-mcp/src/integrationTest/kotlin/mcp/CpgLlmAnalyzeToolTest.kt index 0ec93f35cf1..77c42964569 100644 --- a/cpg-mcp/src/integrationTest/kotlin/mcp/CpgLlmAnalyzeToolTest.kt +++ b/cpg-mcp/src/integrationTest/kotlin/mcp/CpgLlmAnalyzeToolTest.kt @@ -29,9 +29,12 @@ import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.addCpgLlmAnalyzeTool import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.globalAnalysisResult import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.runCpgAnalyze import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalyzePayload -import io.modelcontextprotocol.kotlin.sdk.* +import io.modelcontextprotocol.kotlin.sdk.Implementation +import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse diff --git a/cpg-mcp/src/integrationTest/kotlin/mcp/ListCommandsTest.kt b/cpg-mcp/src/integrationTest/kotlin/mcp/ListCommandsTest.kt index d0c7fb82148..5ede421cee9 100644 --- a/cpg-mcp/src/integrationTest/kotlin/mcp/ListCommandsTest.kt +++ b/cpg-mcp/src/integrationTest/kotlin/mcp/ListCommandsTest.kt @@ -38,12 +38,12 @@ import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.listRecords import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.runCpgAnalyze import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalyzePayload import de.fraunhofer.aisec.cpg.serialization.NodeJSON -import io.modelcontextprotocol.kotlin.sdk.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.Implementation import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities -import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgAnalyzeTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgAnalyzeTool.kt index 4fbde20539c..1d8a0a69468 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgAnalyzeTool.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgAnalyzeTool.kt @@ -37,9 +37,9 @@ import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalyzePayload import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toObject import de.fraunhofer.aisec.cpg.mcp.setupTranslationConfiguration import de.fraunhofer.aisec.cpg.serialization.toJSON -import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import java.io.File import kotlinx.serialization.json.Json diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDataflowTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDataflowTool.kt index c66f2d590b3..c44b7e9cfa1 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDataflowTool.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDataflowTool.kt @@ -39,10 +39,10 @@ import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toObject import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toQueryTreeNode import de.fraunhofer.aisec.cpg.query.May import de.fraunhofer.aisec.cpg.query.dataFlow -import io.modelcontextprotocol.kotlin.sdk.CallToolRequest -import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt index b0cd7fe8d7a..6e6266879da 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt @@ -36,9 +36,9 @@ import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.runCpgAnalyze import de.fraunhofer.aisec.cpg.query.QueryTree import de.fraunhofer.aisec.cpg.serialization.NodeJSON import de.fraunhofer.aisec.cpg.serialization.toJSON -import io.modelcontextprotocol.kotlin.sdk.CallToolRequest -import io.modelcontextprotocol.kotlin.sdk.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import java.util.function.BiFunction import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject From 0a09fdbf06205858caa646287533f908eaf9e515 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 15 Jan 2026 14:26:08 +0100 Subject: [PATCH 45/93] Replace netty with cio --- .../src/integrationTest/kotlin/mcp/ListCommandsTest.kt | 2 +- .../kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt | 4 ++-- .../aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt | 8 +++----- .../aisec/cpg/mcp/mcpserver/tools/ListCommands.kt | 2 +- .../aisec/cpg/mcp/mcpserver/tools/utils/Util.kt | 4 ++-- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/cpg-mcp/src/integrationTest/kotlin/mcp/ListCommandsTest.kt b/cpg-mcp/src/integrationTest/kotlin/mcp/ListCommandsTest.kt index ec9cc297b78..7d0620d22e4 100644 --- a/cpg-mcp/src/integrationTest/kotlin/mcp/ListCommandsTest.kt +++ b/cpg-mcp/src/integrationTest/kotlin/mcp/ListCommandsTest.kt @@ -41,10 +41,10 @@ import de.fraunhofer.aisec.cpg.serialization.NodeJSON import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest -import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams import io.modelcontextprotocol.kotlin.sdk.types.Implementation import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities +import io.modelcontextprotocol.kotlin.sdk.types.TextContent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt index 608607b7169..ab2f801214d 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt @@ -30,8 +30,8 @@ import com.github.ajalt.clikt.core.main import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.int import de.fraunhofer.aisec.cpg.mcp.mcpserver.configureServer +import io.ktor.server.cio.* import io.ktor.server.engine.* -import io.ktor.server.netty.* import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport import io.modelcontextprotocol.kotlin.sdk.server.mcp @@ -86,5 +86,5 @@ fun runMcpServerUsingStdio() { * @param port The port number on which the SSE MCP server will listen for client connections. */ fun runSseMcpServerUsingKtorPlugin(port: Int, server: Server) { - embeddedServer(Netty, host = "0.0.0.0", port = port) { mcp { server } }.start(wait = false) + embeddedServer(CIO, host = "0.0.0.0", port = port) { mcp { server } }.start(wait = false) } diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt index eed9a9e9935..77f947762d2 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt @@ -34,18 +34,16 @@ import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toObject import de.fraunhofer.aisec.cpg.serialization.toJSON import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.types.TextContent -import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams import io.modelcontextprotocol.kotlin.sdk.types.ModelPreferences import io.modelcontextprotocol.kotlin.sdk.types.Role import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonObject fun Server.addCpgLlmAnalyzeTool() { @@ -245,4 +243,4 @@ fun Server.addCpgLlmAnalyzeTool() { ) } } -} \ No newline at end of file +} diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt index bb7abe431fc..49ec8550b4f 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt @@ -29,8 +29,8 @@ import de.fraunhofer.aisec.cpg.TranslationResult import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.concepts.Concept import de.fraunhofer.aisec.cpg.graph.concepts.Operation -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.* import de.fraunhofer.aisec.cpg.graph.invoke +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.* import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt index 4dba53b9f77..6e6266879da 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt @@ -34,11 +34,11 @@ import de.fraunhofer.aisec.cpg.graph.listOverlayClasses import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.globalAnalysisResult import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.runCpgAnalyze import de.fraunhofer.aisec.cpg.query.QueryTree +import de.fraunhofer.aisec.cpg.serialization.NodeJSON +import de.fraunhofer.aisec.cpg.serialization.toJSON import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.TextContent -import de.fraunhofer.aisec.cpg.serialization.NodeJSON -import de.fraunhofer.aisec.cpg.serialization.toJSON import java.util.function.BiFunction import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject From ccca4a36cfd679a76913e1e4f3ea96d8721fd0aa Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Wed, 21 Jan 2026 15:06:57 +0100 Subject: [PATCH 46/93] Add thinking --- .../aisec/codyze/console/ai/McpClient.kt | 27 +- codyze-console/src/main/webapp/pnpm-lock.yaml | 1722 +++++++---------- .../components/ai-agent/ChatInterface.svelte | 59 +- .../src/main/webapp/src/lib/types.ts | 1 + .../webapp/src/routes/ai-agent/+page.svelte | 23 +- .../mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt | 1 - 6 files changed, 815 insertions(+), 1018 deletions(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpClient.kt index e0ae4562c50..4d6475e8c52 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpClient.kt @@ -99,9 +99,9 @@ class McpClient : AutoCloseable { try { val llmResponse = queryLlm( - messages = request.messages, - systemPrompt = request.systemPrompt, - maxTokens = request.maxTokens, + messages = request.params.messages, + systemPrompt = request.params.systemPrompt, + maxTokens = request.params.maxTokens, ) // Return result to server @@ -180,6 +180,11 @@ class McpClient : AutoCloseable { fun chat(request: ChatRequestJSON): Flow = channelFlow { // Check for cancellation before starting ensureActive() + + // Send immediate keepalive to prevent browser timeout while LLM initializes + val keepaliveEvent = buildJsonObject { put("type", "keepalive") } + send(Json.encodeToString(keepaliveEvent)) + val userMessage = request.messages.lastOrNull()?.content ?: "" // Convert MCP tools to LLM format @@ -262,6 +267,22 @@ class McpClient : AutoCloseable { val firstChoice = choices?.firstOrNull()?.jsonObject val delta = firstChoice?.get("delta")?.jsonObject + // Check for reasoning/thinking content first + val reasoning = + delta?.get("reasoning")?.jsonPrimitive?.contentOrNull + if (!reasoning.isNullOrEmpty()) { + val reasoningEvent = buildJsonObject { + put("type", "reasoning") + put("content", reasoning) + } + try { + send(Json.encodeToString(reasoningEvent)) + } catch (e: Exception) { + throw e + } + } + + // Check for regular content val content = delta?.get("content")?.jsonPrimitive?.contentOrNull if (!content.isNullOrEmpty()) { diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 69dc7efddc3..fe768164580 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -10,267 +10,257 @@ importers: dependencies: chart.js: specifier: ^4.4.9 - version: 4.5.0 + version: 4.5.1 marked: specifier: ^16.3.0 - version: 16.3.0 + version: 16.4.2 devDependencies: '@eslint/compat': specifier: ^1.2.5 - version: 1.4.0(eslint@9.27.0(jiti@2.4.2)) + version: 1.4.1(eslint@9.39.2(jiti@2.6.1)) '@eslint/js': specifier: ^9.18.0 - version: 9.27.0 + version: 9.39.2 '@fontsource/noto-sans-mono': specifier: ^5.2.6 - version: 5.2.7 + version: 5.2.10 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.8(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))) + version: 3.0.10(@sveltejs/kit@2.50.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.47.1)(typescript@5.9.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))) '@sveltejs/kit': specifier: ^2.20.6 - version: 2.49.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) + version: 2.50.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.47.1)(typescript@5.9.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 - version: 5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) + version: 5.1.1(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) '@tailwindcss/forms': specifier: ^0.5.10 - version: 0.5.10(tailwindcss@4.1.7) + version: 0.5.11(tailwindcss@4.1.18) '@tailwindcss/typography': specifier: ^0.5.15 - version: 0.5.16(tailwindcss@4.1.7) + version: 0.5.19(tailwindcss@4.1.18) '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.1.7(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) + version: 4.1.18(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) cookie: specifier: 1.1.1 version: 1.1.1 eslint: specifier: ^9.18.0 - version: 9.27.0(jiti@2.4.2) + version: 9.39.2(jiti@2.6.1) eslint-config-prettier: specifier: ^10.0.1 - version: 10.1.5(eslint@9.27.0(jiti@2.4.2)) + version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-svelte: specifier: ^3.0.0 - version: 3.13.0(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4) + version: 3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.47.1) globals: specifier: ^16.0.0 version: 16.5.0 inter-ui: specifier: ^4.1.0 - version: 4.1.0 + version: 4.1.1 prettier: specifier: ^3.5.3 - version: 3.7.4 + version: 3.8.0 prettier-plugin-svelte: specifier: ^3.3.3 - version: 3.4.0(prettier@3.7.4)(svelte@5.33.4) + version: 3.4.1(prettier@3.8.0)(svelte@5.47.1) prettier-plugin-tailwindcss: specifier: ^0.6.11 - version: 0.6.11(prettier-plugin-svelte@3.4.0(prettier@3.7.4)(svelte@5.33.4))(prettier@3.7.4) + version: 0.6.14(prettier-plugin-svelte@3.4.1(prettier@3.8.0)(svelte@5.47.1))(prettier@3.8.0) svelte: specifier: ^5.0.0 - version: 5.33.4 + version: 5.47.1 svelte-check: specifier: ^4.0.0 - version: 4.2.1(picomatch@4.0.3)(svelte@5.33.4)(typescript@5.8.3) + version: 4.3.5(picomatch@4.0.3)(svelte@5.47.1)(typescript@5.9.3) svelte-highlight: specifier: ^7.8.2 - version: 7.8.3 + version: 7.9.0 tailwindcss: specifier: ^4.0.0 - version: 4.1.7 + version: 4.1.18 typescript: specifier: ^5.0.0 - version: 5.8.3 + version: 5.9.3 typescript-eslint: specifier: ^8.20.0 - version: 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) + version: 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^6.0.0 - version: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) + version: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2) packages: - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@esbuild/aix-ppc64@0.25.11': - resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.11': - resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.11': - resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.11': - resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.11': - resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.11': - resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.11': - resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.11': - resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.11': - resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.11': - resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.11': - resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.11': - resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.11': - resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.11': - resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.11': - resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.11': - resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.11': - resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.11': - resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.11': - resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.11': - resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.11': - resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.11': - resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.11': - resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.11': - resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.11': - resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.11': - resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.5.1': - resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/compat@1.4.0': - resolution: {integrity: sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==} + '@eslint/compat@1.4.1': + resolution: {integrity: sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.40 || 9 @@ -278,244 +268,228 @@ packages: eslint: optional: true - '@eslint/config-array@0.20.0': - resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.1': - resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.14.0': - resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.16.0': - resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.27.0': - resolution: {integrity: sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==} + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.1': - resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@fontsource/noto-sans-mono@5.2.7': - resolution: {integrity: sha512-kw+z21ZEjOzsUS49FVLsR/WwJ7LnXrlam5SzNGY6uSSF4gfzYm2CBzcS0Y5YqQCrtDZ4S6PpaAGRMzQ/MAZaXA==} + '@fontsource/noto-sans-mono@5.2.10': + resolution: {integrity: sha512-YG3ev3Bc24UGd1x0THgtU3LG1j+sKahhDgo0vTpEfB0wkXD1+tGSq1fJD+QqnYLXjPhMRNe0ZKWoUi1MPH7Qdg==} '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.6': - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.1': - resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} - engines: {node: '>=18.18'} - - '@humanwhocodes/retry@0.4.2': - resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@rollup/rollup-android-arm-eabi@4.52.5': - resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} + '@rollup/rollup-android-arm-eabi@4.55.3': + resolution: {integrity: sha512-qyX8+93kK/7R5BEXPC2PjUt0+fS/VO2BVHjEHyIEWiYn88rcRBHmdLgoJjktBltgAf+NY7RfCGB1SoyKS/p9kg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.52.5': - resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} + '@rollup/rollup-android-arm64@4.55.3': + resolution: {integrity: sha512-6sHrL42bjt5dHQzJ12Q4vMKfN+kUnZ0atHHnv4V0Wd9JMTk7FDzSY35+7qbz3ypQYMBPANbpGK7JpnWNnhGt8g==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.52.5': - resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} + '@rollup/rollup-darwin-arm64@4.55.3': + resolution: {integrity: sha512-1ht2SpGIjEl2igJ9AbNpPIKzb1B5goXOcmtD0RFxnwNuMxqkR6AUaaErZz+4o+FKmzxcSNBOLrzsICZVNYa1Rw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.52.5': - resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} + '@rollup/rollup-darwin-x64@4.55.3': + resolution: {integrity: sha512-FYZ4iVunXxtT+CZqQoPVwPhH7549e/Gy7PIRRtq4t5f/vt54pX6eG9ebttRH6QSH7r/zxAFA4EZGlQ0h0FvXiA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.52.5': - resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} + '@rollup/rollup-freebsd-arm64@4.55.3': + resolution: {integrity: sha512-M/mwDCJ4wLsIgyxv2Lj7Len+UMHd4zAXu4GQ2UaCdksStglWhP61U3uowkaYBQBhVoNpwx5Hputo8eSqM7K82Q==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.5': - resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} + '@rollup/rollup-freebsd-x64@4.55.3': + resolution: {integrity: sha512-5jZT2c7jBCrMegKYTYTpni8mg8y3uY8gzeq2ndFOANwNuC/xJbVAoGKR9LhMDA0H3nIhvaqUoBEuJoICBudFrA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.52.5': - resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.55.3': + resolution: {integrity: sha512-YeGUhkN1oA+iSPzzhEjVPS29YbViOr8s4lSsFaZKLHswgqP911xx25fPOyE9+khmN6W4VeM0aevbDp4kkEoHiA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.52.5': - resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} + '@rollup/rollup-linux-arm-musleabihf@4.55.3': + resolution: {integrity: sha512-eo0iOIOvcAlWB3Z3eh8pVM8hZ0oVkK3AjEM9nSrkSug2l15qHzF3TOwT0747omI6+CJJvl7drwZepT+re6Fy/w==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.52.5': - resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} + '@rollup/rollup-linux-arm64-gnu@4.55.3': + resolution: {integrity: sha512-DJay3ep76bKUDImmn//W5SvpjRN5LmK/ntWyeJs/dcnwiiHESd3N4uteK9FDLf0S0W8E6Y0sVRXpOCoQclQqNg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.52.5': - resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} + '@rollup/rollup-linux-arm64-musl@4.55.3': + resolution: {integrity: sha512-BKKWQkY2WgJ5MC/ayvIJTHjy0JUGb5efaHCUiG/39sSUvAYRBaO3+/EK0AZT1RF3pSj86O24GLLik9mAYu0IJg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.52.5': - resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} + '@rollup/rollup-linux-loong64-gnu@4.55.3': + resolution: {integrity: sha512-Q9nVlWtKAG7ISW80OiZGxTr6rYtyDSkauHUtvkQI6TNOJjFvpj4gcH+KaJihqYInnAzEEUetPQubRwHef4exVg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.55.3': + resolution: {integrity: sha512-2H5LmhzrpC4fFRNwknzmmTvvyJPHwESoJgyReXeFoYYuIDfBhP29TEXOkCJE/KxHi27mj7wDUClNq78ue3QEBQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.52.5': - resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} + '@rollup/rollup-linux-ppc64-gnu@4.55.3': + resolution: {integrity: sha512-9S542V0ie9LCTznPYlvaeySwBeIEa7rDBgLHKZ5S9DBgcqdJYburabm8TqiqG6mrdTzfV5uttQRHcbKff9lWtA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.52.5': - resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} + '@rollup/rollup-linux-ppc64-musl@4.55.3': + resolution: {integrity: sha512-ukxw+YH3XXpcezLgbJeasgxyTbdpnNAkrIlFGDl7t+pgCxZ89/6n1a+MxlY7CegU+nDgrgdqDelPRNQ/47zs0g==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.55.3': + resolution: {integrity: sha512-Iauw9UsTTvlF++FhghFJjqYxyXdggXsOqGpFBylaRopVpcbfyIIsNvkf9oGwfgIcf57z3m8+/oSYTo6HutBFNw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.52.5': - resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} + '@rollup/rollup-linux-riscv64-musl@4.55.3': + resolution: {integrity: sha512-3OqKAHSEQXKdq9mQ4eajqUgNIK27VZPW3I26EP8miIzuKzCJ3aW3oEn2pzF+4/Hj/Moc0YDsOtBgT5bZ56/vcA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.52.5': - resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} + '@rollup/rollup-linux-s390x-gnu@4.55.3': + resolution: {integrity: sha512-0CM8dSVzVIaqMcXIFej8zZrSFLnGrAE8qlNbbHfTw1EEPnFTg1U1ekI0JdzjPyzSfUsHWtodilQQG/RA55berA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.52.5': - resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} + '@rollup/rollup-linux-x64-gnu@4.55.3': + resolution: {integrity: sha512-+fgJE12FZMIgBaKIAGd45rxf+5ftcycANJRWk8Vz0NnMTM5rADPGuRFTYar+Mqs560xuART7XsX2lSACa1iOmQ==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.52.5': - resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} + '@rollup/rollup-linux-x64-musl@4.55.3': + resolution: {integrity: sha512-tMD7NnbAolWPzQlJQJjVFh/fNH3K/KnA7K8gv2dJWCwwnaK6DFCYST1QXYWfu5V0cDwarWC8Sf/cfMHniNq21A==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.52.5': - resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} + '@rollup/rollup-openbsd-x64@4.55.3': + resolution: {integrity: sha512-u5KsqxOxjEeIbn7bUK1MPM34jrnPwjeqgyin4/N6e/KzXKfpE9Mi0nCxcQjaM9lLmPcHmn/xx1yOjgTMtu1jWQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.55.3': + resolution: {integrity: sha512-vo54aXwjpTtsAnb3ca7Yxs9t2INZg7QdXN/7yaoG7nPGbOBXYXQY41Km+S1Ov26vzOAzLcAjmMdjyEqS1JkVhw==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.52.5': - resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} + '@rollup/rollup-win32-arm64-msvc@4.55.3': + resolution: {integrity: sha512-HI+PIVZ+m+9AgpnY3pt6rinUdRYrGHvmVdsNQ4odNqQ/eRF78DVpMR7mOq7nW06QxpczibwBmeQzB68wJ+4W4A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.52.5': - resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} + '@rollup/rollup-win32-ia32-msvc@4.55.3': + resolution: {integrity: sha512-vRByotbdMo3Wdi+8oC2nVxtc3RkkFKrGaok+a62AT8lz/YBuQjaVYAS5Zcs3tPzW43Vsf9J0wehJbUY5xRSekA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.52.5': - resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} + '@rollup/rollup-win32-x64-gnu@4.55.3': + resolution: {integrity: sha512-POZHq7UeuzMJljC5NjKi8vKMFN6/5EOqcX1yGntNLp7rUTpBAXQ1hW8kWPFxYLv07QMcNM75xqVLGPWQq6TKFA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.5': - resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} + '@rollup/rollup-win32-x64-msvc@4.55.3': + resolution: {integrity: sha512-aPFONczE4fUFKNXszdvnd2GqKEYQdV5oEsIbKPujJmWlCI9zEsv1Otig8RKK+X9bed9gFUN6LAeN4ZcNuu4zjg==} cpu: [x64] os: [win32] - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - - '@sveltejs/acorn-typescript@1.0.5': - resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} - peerDependencies: - acorn: ^8.9.0 + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.7': - resolution: {integrity: sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==} + '@sveltejs/acorn-typescript@1.0.8': + resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} peerDependencies: acorn: ^8.9.0 - '@sveltejs/adapter-static@3.0.8': - resolution: {integrity: sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==} + '@sveltejs/adapter-static@3.0.10': + resolution: {integrity: sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==} peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.49.0': - resolution: {integrity: sha512-oH8tXw7EZnie8FdOWYrF7Yn4IKrqTFHhXvl8YxXxbKwTMcD/5NNCryUSEXRk2ZR4ojnub0P8rNrsVGHXWqIDtA==} + '@sveltejs/kit@2.50.0': + resolution: {integrity: sha512-Hj8sR8O27p2zshFEIJzsvfhLzxga/hWw6tRLnBjMYw70m1aS9BSYCqAUtzDBjRREtX1EvLMYgaC0mYE3Hz4KWA==} engines: {node: '>=18.13'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.0.0 '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 peerDependenciesMeta: '@opentelemetry/api': optional: true + typescript: + optional: true '@sveltejs/vite-plugin-svelte-inspector@4.0.1': resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} @@ -525,77 +499,77 @@ packages: svelte: ^5.0.0 vite: ^6.0.0 - '@sveltejs/vite-plugin-svelte@5.1.0': - resolution: {integrity: sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==} + '@sveltejs/vite-plugin-svelte@5.1.1': + resolution: {integrity: sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22} peerDependencies: svelte: ^5.0.0 vite: ^6.0.0 - '@tailwindcss/forms@0.5.10': - resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==} + '@tailwindcss/forms@0.5.11': + resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} peerDependencies: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' - '@tailwindcss/node@4.1.7': - resolution: {integrity: sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==} + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} - '@tailwindcss/oxide-android-arm64@4.1.7': - resolution: {integrity: sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==} + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.7': - resolution: {integrity: sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==} + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.7': - resolution: {integrity: sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==} + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.7': - resolution: {integrity: sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==} + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': - resolution: {integrity: sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': - resolution: {integrity: sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.7': - resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.7': - resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.7': - resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==} + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.7': - resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==} + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -606,99 +580,98 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': - resolution: {integrity: sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.7': - resolution: {integrity: sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.7': - resolution: {integrity: sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==} + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} engines: {node: '>= 10'} - '@tailwindcss/typography@0.5.16': - resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' - '@tailwindcss/vite@4.1.7': - resolution: {integrity: sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==} + '@tailwindcss/vite@4.1.18': + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} peerDependencies: - vite: ^5.2.0 || ^6 + vite: ^5.2.0 || ^6 || ^7 '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@typescript-eslint/eslint-plugin@8.33.0': - resolution: {integrity: sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==} + '@typescript-eslint/eslint-plugin@8.53.1': + resolution: {integrity: sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.33.0 + '@typescript-eslint/parser': ^8.53.1 eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.33.0': - resolution: {integrity: sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==} + '@typescript-eslint/parser@8.53.1': + resolution: {integrity: sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.33.0': - resolution: {integrity: sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==} + '@typescript-eslint/project-service@8.53.1': + resolution: {integrity: sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.33.0': - resolution: {integrity: sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==} + '@typescript-eslint/scope-manager@8.53.1': + resolution: {integrity: sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.33.0': - resolution: {integrity: sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==} + '@typescript-eslint/tsconfig-utils@8.53.1': + resolution: {integrity: sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.33.0': - resolution: {integrity: sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==} + '@typescript-eslint/type-utils@8.53.1': + resolution: {integrity: sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.33.0': - resolution: {integrity: sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==} + '@typescript-eslint/types@8.53.1': + resolution: {integrity: sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.33.0': - resolution: {integrity: sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==} + '@typescript-eslint/typescript-estree@8.53.1': + resolution: {integrity: sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.33.0': - resolution: {integrity: sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==} + '@typescript-eslint/utils@8.53.1': + resolution: {integrity: sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.33.0': - resolution: {integrity: sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==} + '@typescript-eslint/visitor-keys@8.53.1': + resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -706,11 +679,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -737,15 +705,11 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -755,18 +719,14 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chart.js@4.5.0: - resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==} + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} engines: {pnpm: '>=8'} chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -798,17 +758,8 @@ packages: engines: {node: '>=4'} hasBin: true - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -823,19 +774,19 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.5.0: - resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} + devalue@5.6.2: + resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} - esbuild@0.25.11: - resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true @@ -843,14 +794,14 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-prettier@10.1.5: - resolution: {integrity: sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true peerDependencies: eslint: '>=7.0.0' - eslint-plugin-svelte@3.13.0: - resolution: {integrity: sha512-2ohCCQJJTNbIpQCSDSTWj+FN0OVfPmSO03lmSNT7ytqMaWF6kpT86LdzDqtm4sh7TVPl/OEWJ/d7R87bXP2Vjg==} + eslint-plugin-svelte@3.14.0: + resolution: {integrity: sha512-Isw0GvaMm0yHxAj71edAdGFh28ufYs+6rk2KlbbZphnqZAzrH3Se3t12IFh2H9+1F/jlDhBBL4oiOJmLqmYX0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 @@ -859,10 +810,6 @@ packages: svelte: optional: true - eslint-scope@8.3.0: - resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -871,16 +818,12 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@4.2.1: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.27.0: - resolution: {integrity: sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==} + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -892,20 +835,16 @@ packages: esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - espree@10.3.0: - resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} - esrap@1.4.6: - resolution: {integrity: sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==} + esrap@2.2.2: + resolution: {integrity: sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -922,27 +861,12 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - - fdir@6.4.3: - resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -956,10 +880,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -976,10 +896,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -995,9 +911,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1010,8 +923,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.4: - resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} import-fresh@3.3.1: @@ -1022,8 +935,8 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inter-ui@4.1.0: - resolution: {integrity: sha512-lUJ5YLtpj7jWsrhTbN5w32MLOqISGFGOTMRNn+IGhLrbt67z5yvmBRoqpLCWLVlJ+l7kqnH6su4meEqvLMfAAA==} + inter-ui@4.1.1: + resolution: {integrity: sha512-451h0J29HyOmA+JXgSi/6M12tL7ZCZ8arYKZUXiOXTJpJbAKqJvFh3k5SiV3x7tKe0C0KyrKUUiQIvvZ2PQDcA==} engines: {node: '>=16.0.0'} is-extglob@2.1.1: @@ -1034,22 +947,18 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true json-buffer@3.0.1: @@ -1075,68 +984,74 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lightningcss-darwin-arm64@1.30.1: - resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.30.1: - resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.30.1: - resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.30.1: - resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.30.1: - resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.30.1: - resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.30.1: - resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.30.1: - resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.30.1: - resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.30.1: - resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.30.1: - resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} lilconfig@2.1.0: @@ -1150,34 +1065,17 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.castarray@4.4.0: - resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} - - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - marked@16.3.0: - resolution: {integrity: sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==} + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} engines: {node: '>= 20'} hasBin: true - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - mini-svg-data-uri@1.4.4: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true @@ -1189,19 +1087,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - minizlib@3.0.2: - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} - engines: {node: '>= 18'} - - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -1248,10 +1133,6 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -1284,8 +1165,8 @@ packages: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} - postcss-selector-parser@7.1.0: - resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} postcss@8.5.6: @@ -1296,17 +1177,19 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-svelte@3.4.0: - resolution: {integrity: sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==} + prettier-plugin-svelte@3.4.1: + resolution: {integrity: sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier-plugin-tailwindcss@0.6.11: - resolution: {integrity: sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==} + prettier-plugin-tailwindcss@0.6.14: + resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==} engines: {node: '>=14.21.3'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' '@prettier/plugin-pug': '*' '@shopify/prettier-plugin-liquid': '*' '@trivago/prettier-plugin-sort-imports': '*' @@ -1326,6 +1209,10 @@ packages: peerDependenciesMeta: '@ianvs/prettier-plugin-sort-imports': optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true '@prettier/plugin-pug': optional: true '@shopify/prettier-plugin-liquid': @@ -1357,8 +1244,8 @@ packages: prettier-plugin-svelte: optional: true - prettier@3.7.4: - resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + prettier@3.8.0: + resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==} engines: {node: '>=14'} hasBin: true @@ -1366,9 +1253,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -1377,18 +1261,11 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup@4.52.5: - resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} + rollup@4.55.3: + resolution: {integrity: sha512-y9yUpfQvetAjiDLtNMf1hL9NXchIJgWt6zIKeoB+tCd3npX08Eqfzg60V9DhIGVMtQ0AlMkFw5xa+AQ37zxnAA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -1425,55 +1302,47 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - svelte-check@4.2.1: - resolution: {integrity: sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA==} + svelte-check@4.3.5: + resolution: {integrity: sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte-eslint-parser@1.4.0: - resolution: {integrity: sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.18.3} + svelte-eslint-parser@1.4.1: + resolution: {integrity: sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.24.0} peerDependencies: svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: svelte: optional: true - svelte-highlight@7.8.3: - resolution: {integrity: sha512-i4CE/6yda1fCh0ovUVATk1S1feu1y3+CV+l1brgtMPPRO9VTGq+hPpUjVEJWQkE7hPAgwgVpHccoa5M2gpKxYQ==} + svelte-highlight@7.9.0: + resolution: {integrity: sha512-226LBTtvTnM2L2JkQq8mZeKEeMfPLYyta7VxZatFT4UPX5zdHEerKeMTvrfbxm7MVTWc7TPThsNoVdhWC177KQ==} - svelte@5.33.4: - resolution: {integrity: sha512-w2+ksGaZRgZgQdP2dtOjlpkdQwX3ftc8+BH/W9XgX6rP1FZHCeuXQnmyeHPFLbG2Gjj9pp2ak8iIeVure+n7IA==} + svelte@5.47.1: + resolution: {integrity: sha512-MhSWfWEpG5T57z0Oyfk9D1GhAz/KTZKZZlWtGEsy9zNk2fafpuU7sJQlXNSA8HtvwKxVC9XlDyl5YovXUXjjHA==} engines: {node: '>=18'} - tailwindcss@4.1.7: - resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==} + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} - tapable@2.2.2: - resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} - engines: {node: '>=18'} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -1482,15 +1351,15 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.33.0: - resolution: {integrity: sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==} + typescript-eslint@8.53.1: + resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -1540,10 +1409,10 @@ packages: yaml: optional: true - vitefu@1.0.6: - resolution: {integrity: sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==} + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 peerDependenciesMeta: vite: optional: true @@ -1557,10 +1426,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -1569,296 +1434,271 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zimmerframe@1.1.2: - resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} snapshots: - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - - '@esbuild/aix-ppc64@0.25.11': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/android-arm64@0.25.11': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm@0.25.11': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-x64@0.25.11': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.25.11': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-x64@0.25.11': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.25.11': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.25.11': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/linux-arm64@0.25.11': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm@0.25.11': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-ia32@0.25.11': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-loong64@0.25.11': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-mips64el@0.25.11': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-ppc64@0.25.11': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.25.11': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-s390x@0.25.11': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-x64@0.25.11': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.25.11': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.25.11': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.25.11': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.25.11': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.25.11': + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/sunos-x64@0.25.11': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/win32-arm64@0.25.11': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-ia32@0.25.11': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-x64@0.25.11': + '@esbuild/win32-x64@0.25.12': optional: true - '@eslint-community/eslint-utils@4.5.1(eslint@9.27.0(jiti@2.4.2))': - dependencies: - eslint: 9.27.0(jiti@2.4.2) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/eslint-utils@4.9.0(eslint@9.27.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': dependencies: - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.39.2(jiti@2.6.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.4.0(eslint@9.27.0(jiti@2.4.2))': + '@eslint/compat@1.4.1(eslint@9.39.2(jiti@2.6.1))': dependencies: - '@eslint/core': 0.16.0 + '@eslint/core': 0.17.0 optionalDependencies: - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.39.2(jiti@2.6.1) - '@eslint/config-array@0.20.0': + '@eslint/config-array@0.21.1': dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.0 + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.1': {} - - '@eslint/core@0.14.0': + '@eslint/config-helpers@0.4.2': dependencies: - '@types/json-schema': 7.0.15 + '@eslint/core': 0.17.0 - '@eslint/core@0.16.0': + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 - debug: 4.4.0 - espree: 10.3.0 + debug: 4.4.3 + espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@9.27.0': {} + '@eslint/js@9.39.2': {} - '@eslint/object-schema@2.1.6': {} + '@eslint/object-schema@2.1.7': {} - '@eslint/plugin-kit@0.3.1': + '@eslint/plugin-kit@0.4.1': dependencies: - '@eslint/core': 0.14.0 + '@eslint/core': 0.17.0 levn: 0.4.1 - '@fontsource/noto-sans-mono@5.2.7': {} + '@fontsource/noto-sans-mono@5.2.10': {} '@humanfs/core@0.19.1': {} - '@humanfs/node@0.16.6': + '@humanfs/node@0.16.7': dependencies: '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/retry': 0.4.3 '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.1': {} - - '@humanwhocodes/retry@0.4.2': {} + '@humanwhocodes/retry@0.4.3': {} - '@isaacs/fs-minipass@4.0.1': + '@jridgewell/gen-mapping@0.3.13': dependencies: - minipass: 7.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/gen-mapping@0.3.8': + '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 '@kurkle/color@0.3.4': {} - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} + '@polka/url@1.0.0-next.29': {} - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + '@rollup/rollup-android-arm-eabi@4.55.3': + optional: true - '@polka/url@1.0.0-next.29': {} + '@rollup/rollup-android-arm64@4.55.3': + optional: true - '@rollup/rollup-android-arm-eabi@4.52.5': + '@rollup/rollup-darwin-arm64@4.55.3': optional: true - '@rollup/rollup-android-arm64@4.52.5': + '@rollup/rollup-darwin-x64@4.55.3': optional: true - '@rollup/rollup-darwin-arm64@4.52.5': + '@rollup/rollup-freebsd-arm64@4.55.3': optional: true - '@rollup/rollup-darwin-x64@4.52.5': + '@rollup/rollup-freebsd-x64@4.55.3': optional: true - '@rollup/rollup-freebsd-arm64@4.52.5': + '@rollup/rollup-linux-arm-gnueabihf@4.55.3': optional: true - '@rollup/rollup-freebsd-x64@4.52.5': + '@rollup/rollup-linux-arm-musleabihf@4.55.3': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + '@rollup/rollup-linux-arm64-gnu@4.55.3': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.52.5': + '@rollup/rollup-linux-arm64-musl@4.55.3': optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.5': + '@rollup/rollup-linux-loong64-gnu@4.55.3': optional: true - '@rollup/rollup-linux-arm64-musl@4.52.5': + '@rollup/rollup-linux-loong64-musl@4.55.3': optional: true - '@rollup/rollup-linux-loong64-gnu@4.52.5': + '@rollup/rollup-linux-ppc64-gnu@4.55.3': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.5': + '@rollup/rollup-linux-ppc64-musl@4.55.3': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.52.5': + '@rollup/rollup-linux-riscv64-gnu@4.55.3': optional: true - '@rollup/rollup-linux-riscv64-musl@4.52.5': + '@rollup/rollup-linux-riscv64-musl@4.55.3': optional: true - '@rollup/rollup-linux-s390x-gnu@4.52.5': + '@rollup/rollup-linux-s390x-gnu@4.55.3': optional: true - '@rollup/rollup-linux-x64-gnu@4.52.5': + '@rollup/rollup-linux-x64-gnu@4.55.3': optional: true - '@rollup/rollup-linux-x64-musl@4.52.5': + '@rollup/rollup-linux-x64-musl@4.55.3': optional: true - '@rollup/rollup-openharmony-arm64@4.52.5': + '@rollup/rollup-openbsd-x64@4.55.3': optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.5': + '@rollup/rollup-openharmony-arm64@4.55.3': optional: true - '@rollup/rollup-win32-ia32-msvc@4.52.5': + '@rollup/rollup-win32-arm64-msvc@4.55.3': optional: true - '@rollup/rollup-win32-x64-gnu@4.52.5': + '@rollup/rollup-win32-ia32-msvc@4.55.3': optional: true - '@rollup/rollup-win32-x64-msvc@4.52.5': + '@rollup/rollup-win32-x64-gnu@4.55.3': optional: true - '@standard-schema/spec@1.0.0': {} + '@rollup/rollup-win32-x64-msvc@4.55.3': + optional: true - '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1)': - dependencies: - acorn: 8.14.1 + '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.7(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.50.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.47.1)(typescript@5.9.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))': dependencies: - '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.50.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.47.1)(typescript@5.9.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) - '@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/kit@2.50.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.47.1)(typescript@5.9.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: - '@standard-schema/spec': 1.0.0 - '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.5.0 + devalue: 5.6.2 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -1866,221 +1706,212 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.2 sirv: 3.0.2 - svelte: 5.33.4 - vite: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) + svelte: 5.47.1 + vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2) + optionalDependencies: + typescript: 5.9.3 - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) - debug: 4.4.1 - svelte: 5.33.4 - vite: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) + debug: 4.4.3 + svelte: 5.47.1 + vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.4)(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) - debug: 4.4.1 + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.47.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) + debug: 4.4.3 deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.17 - svelte: 5.33.4 - vite: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) - vitefu: 1.0.6(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)) + magic-string: 0.30.21 + svelte: 5.47.1 + vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2) + vitefu: 1.1.1(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)) transitivePeerDependencies: - supports-color - '@tailwindcss/forms@0.5.10(tailwindcss@4.1.7)': + '@tailwindcss/forms@0.5.11(tailwindcss@4.1.18)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 4.1.7 + tailwindcss: 4.1.18 - '@tailwindcss/node@4.1.7': + '@tailwindcss/node@4.1.18': dependencies: - '@ampproject/remapping': 2.3.0 - enhanced-resolve: 5.18.1 - jiti: 2.4.2 - lightningcss: 1.30.1 + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + lightningcss: 1.30.2 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.7 + tailwindcss: 4.1.18 - '@tailwindcss/oxide-android-arm64@4.1.7': + '@tailwindcss/oxide-android-arm64@4.1.18': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.7': + '@tailwindcss/oxide-darwin-arm64@4.1.18': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.7': + '@tailwindcss/oxide-darwin-x64@4.1.18': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.7': + '@tailwindcss/oxide-freebsd-x64@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.7': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.7': + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.7': + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.7': + '@tailwindcss/oxide-linux-x64-musl@4.1.18': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.7': + '@tailwindcss/oxide-wasm32-wasi@4.1.18': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.7': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.7': + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': optional: true - '@tailwindcss/oxide@4.1.7': - dependencies: - detect-libc: 2.0.4 - tar: 7.4.3 + '@tailwindcss/oxide@4.1.18': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.7 - '@tailwindcss/oxide-darwin-arm64': 4.1.7 - '@tailwindcss/oxide-darwin-x64': 4.1.7 - '@tailwindcss/oxide-freebsd-x64': 4.1.7 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.7 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.7 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.7 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.7 - '@tailwindcss/oxide-linux-x64-musl': 4.1.7 - '@tailwindcss/oxide-wasm32-wasi': 4.1.7 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.7 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.7 - - '@tailwindcss/typography@0.5.16(tailwindcss@4.1.7)': - dependencies: - lodash.castarray: 4.4.0 - lodash.isplainobject: 4.0.6 - lodash.merge: 4.6.2 + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.1.18)': + dependencies: postcss-selector-parser: 6.0.10 - tailwindcss: 4.1.7 + tailwindcss: 4.1.18 - '@tailwindcss/vite@4.1.7(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1))': + '@tailwindcss/vite@4.1.18(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: - '@tailwindcss/node': 4.1.7 - '@tailwindcss/oxide': 4.1.7 - tailwindcss: 4.1.7 - vite: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + tailwindcss: 4.1.18 + vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2) '@types/cookie@0.6.0': {} - '@types/estree@1.0.6': {} - '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} - '@typescript-eslint/eslint-plugin@8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.33.0 - '@typescript-eslint/type-utils': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.33.0 - eslint: 9.27.0(jiti@2.4.2) - graphemer: 1.4.0 - ignore: 7.0.4 + '@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.1 + eslint: 9.39.2(jiti@2.6.1) + ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.33.0 - '@typescript-eslint/types': 8.33.0 - '@typescript-eslint/typescript-estree': 8.33.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.33.0 - debug: 4.4.1 - eslint: 9.27.0(jiti@2.4.2) - typescript: 5.8.3 + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.1 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.33.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.53.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.33.0(typescript@5.8.3) - '@typescript-eslint/types': 8.33.0 - debug: 4.4.1 + '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3) + '@typescript-eslint/types': 8.53.1 + debug: 4.4.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/scope-manager@8.33.0': + '@typescript-eslint/scope-manager@8.53.1': dependencies: - '@typescript-eslint/types': 8.33.0 - '@typescript-eslint/visitor-keys': 8.33.0 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/visitor-keys': 8.53.1 - '@typescript-eslint/tsconfig-utils@8.33.0(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.53.1(typescript@5.9.3)': dependencies: - typescript: 5.8.3 + typescript: 5.9.3 - '@typescript-eslint/type-utils@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.33.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.1 - eslint: 9.27.0(jiti@2.4.2) - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.33.0': {} + '@typescript-eslint/types@8.53.1': {} - '@typescript-eslint/typescript-estree@8.33.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.53.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.33.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.33.0(typescript@5.8.3) - '@typescript-eslint/types': 8.33.0 - '@typescript-eslint/visitor-keys': 8.33.0 - debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 + '@typescript-eslint/project-service': 8.53.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3) + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/visitor-keys': 8.53.1 + debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.27.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.33.0 - '@typescript-eslint/types': 8.33.0 - '@typescript-eslint/typescript-estree': 8.33.0(typescript@5.8.3) - eslint: 9.27.0(jiti@2.4.2) - typescript: 5.8.3 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.33.0': + '@typescript-eslint/visitor-keys@8.53.1': dependencies: - '@typescript-eslint/types': 8.33.0 + '@typescript-eslint/types': 8.53.1 eslint-visitor-keys: 4.2.1 acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 - acorn@8.14.1: {} - acorn@8.15.0: {} ajv@6.12.6: @@ -2102,19 +1933,15 @@ snapshots: balanced-match@1.0.2: {} - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - callsites@3.1.0: {} chalk@4.1.2: @@ -2122,7 +1949,7 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chart.js@4.5.0: + chart.js@4.5.1: dependencies: '@kurkle/color': 0.3.4 @@ -2130,8 +1957,6 @@ snapshots: dependencies: readdirp: 4.1.2 - chownr@3.0.0: {} - clsx@2.1.1: {} color-convert@2.0.1: @@ -2154,11 +1979,7 @@ snapshots: cssesc@3.0.0: {} - debug@4.4.0: - dependencies: - ms: 2.1.3 - - debug@4.4.1: + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2166,55 +1987,55 @@ snapshots: deepmerge@4.3.1: {} - detect-libc@2.0.4: {} + detect-libc@2.1.2: {} - devalue@5.5.0: {} + devalue@5.6.2: {} - enhanced-resolve@5.18.1: + enhanced-resolve@5.18.4: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.2 + tapable: 2.3.0 - esbuild@0.25.11: + esbuild@0.25.12: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.11 - '@esbuild/android-arm': 0.25.11 - '@esbuild/android-arm64': 0.25.11 - '@esbuild/android-x64': 0.25.11 - '@esbuild/darwin-arm64': 0.25.11 - '@esbuild/darwin-x64': 0.25.11 - '@esbuild/freebsd-arm64': 0.25.11 - '@esbuild/freebsd-x64': 0.25.11 - '@esbuild/linux-arm': 0.25.11 - '@esbuild/linux-arm64': 0.25.11 - '@esbuild/linux-ia32': 0.25.11 - '@esbuild/linux-loong64': 0.25.11 - '@esbuild/linux-mips64el': 0.25.11 - '@esbuild/linux-ppc64': 0.25.11 - '@esbuild/linux-riscv64': 0.25.11 - '@esbuild/linux-s390x': 0.25.11 - '@esbuild/linux-x64': 0.25.11 - '@esbuild/netbsd-arm64': 0.25.11 - '@esbuild/netbsd-x64': 0.25.11 - '@esbuild/openbsd-arm64': 0.25.11 - '@esbuild/openbsd-x64': 0.25.11 - '@esbuild/openharmony-arm64': 0.25.11 - '@esbuild/sunos-x64': 0.25.11 - '@esbuild/win32-arm64': 0.25.11 - '@esbuild/win32-ia32': 0.25.11 - '@esbuild/win32-x64': 0.25.11 + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.5(eslint@9.27.0(jiti@2.4.2)): + eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.39.2(jiti@2.6.1) - eslint-plugin-svelte@3.13.0(eslint@9.27.0(jiti@2.4.2))(svelte@5.33.4): + eslint-plugin-svelte@3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.47.1): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.27.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 9.27.0(jiti@2.4.2) + eslint: 9.39.2(jiti@2.6.1) esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 @@ -2222,17 +2043,12 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) semver: 7.7.3 - svelte-eslint-parser: 1.4.0(svelte@5.33.4) + svelte-eslint-parser: 1.4.1(svelte@5.47.1) optionalDependencies: - svelte: 5.33.4 + svelte: 5.47.1 transitivePeerDependencies: - ts-node - eslint-scope@8.3.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -2240,34 +2056,31 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.0: {} - eslint-visitor-keys@4.2.1: {} - eslint@9.27.0(jiti@2.4.2): - dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.27.0(jiti@2.4.2)) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.1 - '@eslint/core': 0.14.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.27.0 - '@eslint/plugin-kit': 0.3.1 - '@humanfs/node': 0.16.6 + eslint@9.39.2(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.2 - '@types/estree': 1.0.6 - '@types/json-schema': 7.0.15 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 - esquery: 1.6.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -2282,31 +2095,25 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.4.2 + jiti: 2.6.1 transitivePeerDependencies: - supports-color esm-env@1.2.2: {} - espree@10.3.0: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 4.2.0 - espree@10.4.0: dependencies: acorn: 8.15.0 acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.1 - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 - esrap@1.4.6: + esrap@2.2.2: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 esrecurse@4.3.0: dependencies: @@ -2318,26 +2125,10 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - - fdir@6.4.3(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -2346,10 +2137,6 @@ snapshots: dependencies: flat-cache: 4.0.1 - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -2365,10 +2152,6 @@ snapshots: fsevents@2.3.3: optional: true - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -2379,15 +2162,13 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - has-flag@4.0.0: {} highlight.js@11.11.1: {} ignore@5.3.2: {} - ignore@7.0.4: {} + ignore@7.0.5: {} import-fresh@3.3.1: dependencies: @@ -2396,7 +2177,7 @@ snapshots: imurmurhash@0.1.4: {} - inter-ui@4.1.0: {} + inter-ui@4.1.1: {} is-extglob@2.1.1: {} @@ -2404,17 +2185,15 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-number@7.0.0: {} - is-reference@3.0.3: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 isexe@2.0.0: {} - jiti@2.4.2: {} + jiti@2.6.1: {} - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -2437,50 +2216,54 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lightningcss-darwin-arm64@1.30.1: + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: optional: true - lightningcss-darwin-x64@1.30.1: + lightningcss-darwin-x64@1.30.2: optional: true - lightningcss-freebsd-x64@1.30.1: + lightningcss-freebsd-x64@1.30.2: optional: true - lightningcss-linux-arm-gnueabihf@1.30.1: + lightningcss-linux-arm-gnueabihf@1.30.2: optional: true - lightningcss-linux-arm64-gnu@1.30.1: + lightningcss-linux-arm64-gnu@1.30.2: optional: true - lightningcss-linux-arm64-musl@1.30.1: + lightningcss-linux-arm64-musl@1.30.2: optional: true - lightningcss-linux-x64-gnu@1.30.1: + lightningcss-linux-x64-gnu@1.30.2: optional: true - lightningcss-linux-x64-musl@1.30.1: + lightningcss-linux-x64-musl@1.30.2: optional: true - lightningcss-win32-arm64-msvc@1.30.1: + lightningcss-win32-arm64-msvc@1.30.2: optional: true - lightningcss-win32-x64-msvc@1.30.1: + lightningcss-win32-x64-msvc@1.30.2: optional: true - lightningcss@1.30.1: + lightningcss@1.30.2: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 optionalDependencies: - lightningcss-darwin-arm64: 1.30.1 - lightningcss-darwin-x64: 1.30.1 - lightningcss-freebsd-x64: 1.30.1 - lightningcss-linux-arm-gnueabihf: 1.30.1 - lightningcss-linux-arm64-gnu: 1.30.1 - lightningcss-linux-arm64-musl: 1.30.1 - lightningcss-linux-x64-gnu: 1.30.1 - lightningcss-linux-x64-musl: 1.30.1 - lightningcss-win32-arm64-msvc: 1.30.1 - lightningcss-win32-x64-msvc: 1.30.1 + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 lilconfig@2.1.0: {} @@ -2490,46 +2273,23 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.castarray@4.4.0: {} - - lodash.isplainobject@4.0.6: {} - lodash.merge@4.6.2: {} - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - marked@16.3.0: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 + marked@16.4.2: {} mini-svg-data-uri@1.4.4: {} minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 - - minipass@7.1.2: {} - - minizlib@3.0.2: - dependencies: - minipass: 7.1.2 - - mkdirp@3.0.1: {} + brace-expansion: 2.0.2 mri@1.2.0: {} @@ -2568,8 +2328,6 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} - picomatch@4.0.3: {} postcss-load-config@3.1.4(postcss@8.5.6): @@ -2592,7 +2350,7 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.0: + postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 @@ -2605,61 +2363,56 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.4.0(prettier@3.7.4)(svelte@5.33.4): + prettier-plugin-svelte@3.4.1(prettier@3.8.0)(svelte@5.47.1): dependencies: - prettier: 3.7.4 - svelte: 5.33.4 + prettier: 3.8.0 + svelte: 5.47.1 - prettier-plugin-tailwindcss@0.6.11(prettier-plugin-svelte@3.4.0(prettier@3.7.4)(svelte@5.33.4))(prettier@3.7.4): + prettier-plugin-tailwindcss@0.6.14(prettier-plugin-svelte@3.4.1(prettier@3.8.0)(svelte@5.47.1))(prettier@3.8.0): dependencies: - prettier: 3.7.4 + prettier: 3.8.0 optionalDependencies: - prettier-plugin-svelte: 3.4.0(prettier@3.7.4)(svelte@5.33.4) + prettier-plugin-svelte: 3.4.1(prettier@3.8.0)(svelte@5.47.1) - prettier@3.7.4: {} + prettier@3.8.0: {} punycode@2.3.1: {} - queue-microtask@1.2.3: {} - readdirp@4.1.2: {} resolve-from@4.0.0: {} - reusify@1.1.0: {} - - rollup@4.52.5: + rollup@4.55.3: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.5 - '@rollup/rollup-android-arm64': 4.52.5 - '@rollup/rollup-darwin-arm64': 4.52.5 - '@rollup/rollup-darwin-x64': 4.52.5 - '@rollup/rollup-freebsd-arm64': 4.52.5 - '@rollup/rollup-freebsd-x64': 4.52.5 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 - '@rollup/rollup-linux-arm-musleabihf': 4.52.5 - '@rollup/rollup-linux-arm64-gnu': 4.52.5 - '@rollup/rollup-linux-arm64-musl': 4.52.5 - '@rollup/rollup-linux-loong64-gnu': 4.52.5 - '@rollup/rollup-linux-ppc64-gnu': 4.52.5 - '@rollup/rollup-linux-riscv64-gnu': 4.52.5 - '@rollup/rollup-linux-riscv64-musl': 4.52.5 - '@rollup/rollup-linux-s390x-gnu': 4.52.5 - '@rollup/rollup-linux-x64-gnu': 4.52.5 - '@rollup/rollup-linux-x64-musl': 4.52.5 - '@rollup/rollup-openharmony-arm64': 4.52.5 - '@rollup/rollup-win32-arm64-msvc': 4.52.5 - '@rollup/rollup-win32-ia32-msvc': 4.52.5 - '@rollup/rollup-win32-x64-gnu': 4.52.5 - '@rollup/rollup-win32-x64-msvc': 4.52.5 + '@rollup/rollup-android-arm-eabi': 4.55.3 + '@rollup/rollup-android-arm64': 4.55.3 + '@rollup/rollup-darwin-arm64': 4.55.3 + '@rollup/rollup-darwin-x64': 4.55.3 + '@rollup/rollup-freebsd-arm64': 4.55.3 + '@rollup/rollup-freebsd-x64': 4.55.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.3 + '@rollup/rollup-linux-arm-musleabihf': 4.55.3 + '@rollup/rollup-linux-arm64-gnu': 4.55.3 + '@rollup/rollup-linux-arm64-musl': 4.55.3 + '@rollup/rollup-linux-loong64-gnu': 4.55.3 + '@rollup/rollup-linux-loong64-musl': 4.55.3 + '@rollup/rollup-linux-ppc64-gnu': 4.55.3 + '@rollup/rollup-linux-ppc64-musl': 4.55.3 + '@rollup/rollup-linux-riscv64-gnu': 4.55.3 + '@rollup/rollup-linux-riscv64-musl': 4.55.3 + '@rollup/rollup-linux-s390x-gnu': 4.55.3 + '@rollup/rollup-linux-x64-gnu': 4.55.3 + '@rollup/rollup-linux-x64-musl': 4.55.3 + '@rollup/rollup-openbsd-x64': 4.55.3 + '@rollup/rollup-openharmony-arm64': 4.55.3 + '@rollup/rollup-win32-arm64-msvc': 4.55.3 + '@rollup/rollup-win32-ia32-msvc': 4.55.3 + '@rollup/rollup-win32-x64-gnu': 4.55.3 + '@rollup/rollup-win32-x64-msvc': 4.55.3 fsevents: 2.3.3 - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - sade@1.8.1: dependencies: mri: 1.2.0 @@ -2688,93 +2441,82 @@ snapshots: dependencies: has-flag: 4.0.0 - svelte-check@4.2.1(picomatch@4.0.3)(svelte@5.33.4)(typescript@5.8.3): + svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.47.1)(typescript@5.9.3): dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 - fdir: 6.4.3(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.33.4 - typescript: 5.8.3 + svelte: 5.47.1 + typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.4.0(svelte@5.33.4): + svelte-eslint-parser@1.4.1(svelte@5.47.1): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 postcss: 8.5.6 postcss-scss: 4.0.9(postcss@8.5.6) - postcss-selector-parser: 7.1.0 + postcss-selector-parser: 7.1.1 optionalDependencies: - svelte: 5.33.4 + svelte: 5.47.1 - svelte-highlight@7.8.3: + svelte-highlight@7.9.0: dependencies: highlight.js: 11.11.1 - svelte@5.33.4: + svelte@5.47.1: dependencies: - '@ampproject/remapping': 2.3.0 - '@jridgewell/sourcemap-codec': 1.5.0 - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.1) - '@types/estree': 1.0.6 - acorn: 8.14.1 + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 + devalue: 5.6.2 esm-env: 1.2.2 - esrap: 1.4.6 + esrap: 2.2.2 is-reference: 3.0.3 locate-character: 3.0.0 - magic-string: 0.30.17 - zimmerframe: 1.1.2 - - tailwindcss@4.1.7: {} + magic-string: 0.30.21 + zimmerframe: 1.1.4 - tapable@2.2.2: {} + tailwindcss@4.1.18: {} - tar@7.4.3: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.0.2 - mkdirp: 3.0.1 - yallist: 5.0.0 + tapable@2.3.0: {} tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - totalist@3.0.1: {} - ts-api-utils@2.1.0(typescript@5.8.3): + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: - typescript: 5.8.3 + typescript: 5.9.3 type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3): + typescript-eslint@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.27.0(jiti@2.4.2) - typescript: 5.8.3 + '@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - typescript@5.8.3: {} + typescript@5.9.3: {} uri-js@4.4.1: dependencies: @@ -2782,22 +2524,22 @@ snapshots: util-deprecate@1.0.2: {} - vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1): + vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2): dependencies: - esbuild: 0.25.11 + esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.5 + rollup: 4.55.3 tinyglobby: 0.2.15 optionalDependencies: fsevents: 2.3.3 - jiti: 2.4.2 - lightningcss: 1.30.1 + jiti: 2.6.1 + lightningcss: 1.30.2 - vitefu@1.0.6(vite@6.4.1(jiti@2.4.2)(lightningcss@1.30.1)): + vitefu@1.1.1(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)): optionalDependencies: - vite: 6.4.1(jiti@2.4.2)(lightningcss@1.30.1) + vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2) which@2.0.2: dependencies: @@ -2805,10 +2547,8 @@ snapshots: word-wrap@1.2.5: {} - yallist@5.0.0: {} - yaml@1.10.2: {} yocto-queue@0.1.0: {} - zimmerframe@1.1.2: {} + zimmerframe@1.1.4: {} diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte index 1ac0ad79278..409dbde5c4b 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte @@ -61,6 +61,7 @@ currentMessage: string; isLoading: boolean; streamingContent: string; + isThinking: boolean; analysisResult?: AnalysisResultJSON | null; onSendMessage: () => void; onReset: () => void; @@ -72,6 +73,7 @@ currentMessage, isLoading, streamingContent, + isThinking, analysisResult, onSendMessage, onReset, @@ -162,6 +164,41 @@ {:else}
+ + {#if message.reasoning} + {@const messageId = message.id} +
+ + +
+ {/if} {#if message.contentType === 'tool-result' && message.toolResult} @@ -175,27 +212,21 @@ {/if} {/each} - {#if isLoading || displayContent} + {#if isLoading || displayContent || isThinking}
+ {#if displayContent}
{:else} - -
-
-
-
-
-
+ +
+ + + + Thinking...
{/if}
diff --git a/codyze-console/src/main/webapp/src/lib/types.ts b/codyze-console/src/main/webapp/src/lib/types.ts index 9a7945f63bb..8bd061fc3ce 100644 --- a/codyze-console/src/main/webapp/src/lib/types.ts +++ b/codyze-console/src/main/webapp/src/lib/types.ts @@ -87,6 +87,7 @@ export interface ChatMessage { content: string; contentType?: 'text' | 'tool-result'; toolResult?: ToolResult; + reasoning?: string; timestamp: Date; } diff --git a/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte index e85fa24ff8c..70767f0b6ef 100644 --- a/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte +++ b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte @@ -1,6 +1,6 @@ + +
+
+
+ + + + {toolDisplayName} +
+ {items.length} result{items.length !== 1 ? 's' : ''} +
+ +
+ {#if items.length === 0} +
+

No results

+
+ {:else} + {#each items as item (item.id)} +
+ + + {#if item.expandable && expandedIds.has(item.id)} +
+ {#each Object.entries(item.extraProps) as [key, value]} +
+ {key}: + {#if key === 'Code'} +
{value}
+ {:else} +

{value}

+ {/if} +
+ {/each} + +
+ {/if} +
+ {/each} + {/if} +
+
+ + diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/LlmAnalysisWidget.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/LlmAnalysisWidget.svelte deleted file mode 100644 index 4ad6c165462..00000000000 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/LlmAnalysisWidget.svelte +++ /dev/null @@ -1,542 +0,0 @@ - - -
- -
-
- - - - LLM Analysis -
- {suggestions.length} suggestion{suggestions.length !== 1 ? 's' : ''} -
- - -
- {#if summary} -
-
Summary
-

{summary}

-
- {/if} - - {#if suggestions.length > 0} - - {#if conceptSuggestions.length > 0} -
-
- - - - Concepts ({conceptSuggestions.length}) -
-
- {#each conceptSuggestions as suggestion} -
- - - {#if expandedSuggestions.has(suggestion.nodeId)} -
- {#if suggestion.reasoning} -
- Reasoning: -

{suggestion.reasoning}

-
- {/if} - {#if suggestion.securityImpact} -
- Security Impact: -

{suggestion.securityImpact}

-
- {/if} - -
- {/if} -
- {/each} -
-
- {/if} - - - {#if operationSuggestions.length > 0} -
-
- - - - Operations ({operationSuggestions.length}) -
-
- {#each operationSuggestions as suggestion} -
- - - {#if expandedSuggestions.has(suggestion.nodeId)} -
- {#if suggestion.reasoning} -
- Reasoning: -

{suggestion.reasoning}

-
- {/if} - {#if suggestion.securityImpact} -
- Security Impact: -

{suggestion.securityImpact}

-
- {/if} - -
- {/if} -
- {/each} -
-
- {/if} - - - - {:else} -
- - - -

No suggestions generated

-
- {/if} -
-
- - \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/NodeListWidget.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/NodeListWidget.svelte deleted file mode 100644 index 4597b79f61b..00000000000 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/NodeListWidget.svelte +++ /dev/null @@ -1,241 +0,0 @@ - - -
- -
-
- - - - {toolDisplayName} -
- {items.length} result{items.length !== 1 ? 's' : ''} -
- - -
- {#each items as item, idx} - - {/each} -
-
- - \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte index 5a6246016de..3654b8524d8 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte @@ -1,11 +1,6 @@
- {#if selectedWidget} + {#if data.isPending} + +
+
+
+ {data.toolName} + Running +
+
+ Executing tool... +
+
+ {:else if selectedWidget} {@const SelectedWidget = selectedWidget} @@ -136,4 +141,57 @@ white-space: pre-wrap; word-wrap: break-word; } - \ No newline at end of file + + /* Pending state styles */ + .pending-widget { + margin: 0.5rem 0; + background: white; + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + overflow: hidden; + } + + .pending-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: #f0f9ff; + border-bottom: 1px solid #e0f2fe; + } + + .pending-spinner { + width: 1rem; + height: 1rem; + border: 2px solid #bfdbfe; + border-top-color: #3b82f6; + border-radius: 50%; + animation: spin 0.8s linear infinite; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + .pending-badge { + padding: 0.25rem 0.5rem; + background: #dbeafe; + color: #1d4ed8; + font-size: 0.75rem; + font-weight: 600; + border-radius: 0.25rem; + text-transform: uppercase; + } + + .pending-content { + padding: 1rem; + } + + .pending-text { + color: #6b7280; + font-size: 0.875rem; + font-style: italic; + } + diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts index 6a8e31328c1..2279d2c18f6 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts @@ -4,6 +4,7 @@ export type ToolResultData = { toolName?: string; content: any; isError?: boolean; + isPending?: boolean; }; export type WidgetProps = { diff --git a/codyze-console/src/main/webapp/src/lib/types.ts b/codyze-console/src/main/webapp/src/lib/types.ts index 8bd061fc3ce..b3b3521aaf9 100644 --- a/codyze-console/src/main/webapp/src/lib/types.ts +++ b/codyze-console/src/main/webapp/src/lib/types.ts @@ -79,13 +79,14 @@ export interface ToolResult { toolName?: string; content: any; isError?: boolean; + isPending?: boolean; } export interface ChatMessage { id: string; role: 'user' | 'assistant'; content: string; - contentType?: 'text' | 'tool-result'; + contentType?: 'text' | 'tool-result' | 'tool-pending'; toolResult?: ToolResult; reasoning?: string; timestamp: Date; diff --git a/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte index 70767f0b6ef..b5485170dec 100644 --- a/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte +++ b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte @@ -39,7 +39,8 @@ let streamingContent = $state(''); let streamingReasoning = $state(''); let showWelcome = $state(persisted.showWelcome); - let pendingToolResult = $state<{ toolName: string; content: any } | null>(null); + // Track pending tools by name for loading state + let pendingTools = $state>(new Map()); // Save state to sessionStorage whenever it changes $effect(() => { @@ -64,6 +65,7 @@ currentMessage = ''; streamingContent = ''; streamingReasoning = ''; + pendingTools = new Map(); } async function sendMessage() { @@ -100,13 +102,61 @@ } else if (event.type === 'reasoning') { // LLM thinking/reasoning content - accumulate it streamingReasoning += event.content; + } else if (event.type === 'tool_pending') { + // Tool is starting - add a pending message immediately + const pendingMessage: ChatMessage = { + id: `pending-${event.toolName}-${Date.now()}`, + role: 'assistant' as const, + content: '', + contentType: 'tool-pending', + toolResult: { + toolName: event.toolName, + content: null, + isPending: true + }, + timestamp: new Date() + }; + chatMessages = [...chatMessages, pendingMessage]; + // Track this pending tool + pendingTools.set(event.toolName, { toolName: event.toolName, arguments: event.arguments }); } else if (event.type === 'tool_result') { - // Tool result with toolName and content + // Tool completed - replace pending message with result console.log('Received tool_result event:', event); - pendingToolResult = { - toolName: event.toolName, - content: event.content - }; + // Find and update the pending message for this tool + const pendingIdx = chatMessages.findIndex( + m => m.contentType === 'tool-pending' && m.toolResult?.toolName === event.toolName + ); + if (pendingIdx !== -1) { + // Replace pending with actual result + const updatedMessages = [...chatMessages]; + updatedMessages[pendingIdx] = { + ...updatedMessages[pendingIdx], + id: Date.now().toString(), + contentType: 'tool-result', + toolResult: { + toolName: event.toolName, + content: event.content, + isPending: false + } + }; + chatMessages = updatedMessages; + } else { + // No pending message found, add as new + chatMessages = [...chatMessages, { + id: Date.now().toString(), + role: 'assistant' as const, + content: '', + contentType: 'tool-result', + toolResult: { + toolName: event.toolName, + content: event.content, + isPending: false + }, + timestamp: new Date() + }]; + } + // Remove from pending tracking + pendingTools.delete(event.toolName); } } catch (e) { // If JSON parsing fails, treat as plain text @@ -127,42 +177,45 @@ streamingContent = ''; }, onComplete: () => { - // Capture reasoning before reset + // Capture reasoning and attach to first tool message if exists const reasoning = streamingReasoning || undefined; - - // If we have a tool result, show the widget - if (pendingToolResult) { - const toolResultMessage: ChatMessage = { - id: Date.now().toString(), - role: 'assistant' as const, - content: '', - contentType: 'tool-result', - toolResult: pendingToolResult, - reasoning, - timestamp: new Date() - }; - chatMessages = [...chatMessages, toolResultMessage]; + if (reasoning) { + // Find first tool-result message and attach reasoning + const firstToolIdx = chatMessages.findIndex( + m => m.contentType === 'tool-result' || m.contentType === 'tool-pending' + ); + if (firstToolIdx !== -1 && !chatMessages[firstToolIdx].reasoning) { + const updatedMessages = [...chatMessages]; + updatedMessages[firstToolIdx] = { + ...updatedMessages[firstToolIdx], + reasoning + }; + chatMessages = updatedMessages; + } } - // Show text response only if there are NO tool results and we have text + // Add text response (LLM summary) if present const content = streamingContent; - if (!pendingToolResult && content && content.trim().length > 0) { - const textMessage: ChatMessage = { + if (content && content.trim().length > 0) { + const hasToolResults = chatMessages.some( + m => m.contentType === 'tool-result' || m.contentType === 'tool-pending' + ); + chatMessages = [...chatMessages, { id: (Date.now() + 1).toString(), role: 'assistant' as const, content: content, contentType: 'text', - reasoning, + // Attach reasoning to text only if no tool results + reasoning: !hasToolResults ? reasoning : undefined, timestamp: new Date() - }; - chatMessages = [...chatMessages, textMessage]; + }]; } // Reset state isLoading = false; streamingContent = ''; streamingReasoning = ''; - pendingToolResult = null; + pendingTools = new Map(); } }; diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgApplyConceptsTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgApplyConceptsTool.kt index 7748b2edb2a..a2d905d01e7 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgApplyConceptsTool.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgApplyConceptsTool.kt @@ -119,9 +119,9 @@ fun Server.addCpgApplyConceptsTool() { ToolSchema( properties = buildJsonObject { - putJsonObject("assignments") { + putJsonObject("items") { put("type", "array") - put("description", "List of concept assignments to perform") + put("description", "List of overlay suggestions to apply") putJsonObject("items") { put("type", "object") putJsonObject("properties") { @@ -163,7 +163,7 @@ fun Server.addCpgApplyConceptsTool() { } } }, - required = listOf("assignments"), + required = listOf("items"), ) this.addTool( @@ -185,32 +185,32 @@ fun Server.addCpgApplyConceptsTool() { val applied = mutableListOf() - payload.assignments.forEach { assignment -> + payload.items.forEach { suggestion -> try { - val node = result.nodes.find { it.id.toString() == assignment.nodeId } + val node = result.nodes.find { it.id.toString() == suggestion.nodeId } if (node == null) { - applied.add("Node ${assignment.nodeId} not found") + applied.add("Node ${suggestion.nodeId} not found") return@forEach } - when (assignment.overlayType?.lowercase()) { + when (suggestion.overlayType?.lowercase()) { "concept" -> { result.conceptBuildHelper( - name = assignment.overlay, + name = suggestion.overlay, underlyingNode = node, - constructorArguments = /*assignment.arguments ?:*/ + constructorArguments = /*suggestion.arguments ?:*/ emptyMap(), // TODO: handle arguments connectDFGUnderlyingNodeToConcept = true, ) applied.add( - "Applied concept ${assignment.overlay} to node ${assignment.nodeId} (${node::class.simpleName})" + "Applied concept ${suggestion.overlay} to node ${suggestion.nodeId} (${node::class.simpleName})" ) } "operation" -> { - val conceptNodeId = assignment.conceptNodeId + val conceptNodeId = suggestion.conceptNodeId if (conceptNodeId == null) { applied.add( - "Cannot apply operation ${assignment.overlay} to node ${assignment.nodeId}: conceptNodeId is required for operations" + "Cannot apply operation ${suggestion.overlay} to node ${suggestion.nodeId}: conceptNodeId is required for operations" ) return@forEach } @@ -221,38 +221,37 @@ fun Server.addCpgApplyConceptsTool() { conceptNode?.overlays?.filterIsInstance()?.firstOrNull() if (concept == null) { applied.add( - "Cannot apply operation ${assignment.overlay} to node ${assignment.nodeId}: No concept found on node $conceptNodeId" + "Cannot apply operation ${suggestion.overlay} to node ${suggestion.nodeId}: No concept found on node $conceptNodeId" ) return@forEach } result.operationBuildHelper( - name = assignment.overlay, + name = suggestion.overlay, underlyingNode = node, concept = concept, - constructorArguments = /*assignment.arguments ?:*/ + constructorArguments = /*suggestion.arguments ?:*/ emptyMap(), // TODO: handle arguments connectDFGUnderlyingNodeToConcept = true, ) applied.add( - "Applied operation ${assignment.overlay} to node ${assignment.nodeId} with concept ${concept::class.simpleName}" + "Applied operation ${suggestion.overlay} to node ${suggestion.nodeId} with concept ${concept::class.simpleName}" ) } else -> { applied.add( - "Unknown overlay type '${assignment.overlayType}' for node ${assignment.nodeId}" + "Unknown overlay type '${suggestion.overlayType}' for node ${suggestion.nodeId}" ) } } } catch (e: Exception) { applied.add( - "Failed to create ${assignment.overlayType} ${assignment.overlay} for node ${assignment.nodeId}: ${e.message}" + "Failed to create ${suggestion.overlayType} ${suggestion.overlay} for node ${suggestion.nodeId}: ${e.message}" ) } } - val summary = - "Applied ${payload.assignments.size} concept(s):\n" + applied.joinToString("\n") + val summary = "Applied ${payload.items.size} concept(s):\n" + applied.joinToString("\n") CallToolResult(content = listOf(TextContent(summary))) } diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt index d7028e88116..6cb20cc80a1 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt @@ -28,6 +28,7 @@ package de.fraunhofer.aisec.cpg.mcp.mcpserver.tools import de.fraunhofer.aisec.cpg.graph.nodes import de.fraunhofer.aisec.cpg.mcp.mcpserver.cpgDescription import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgLlmAnalyzePayload +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.OverlaySuggestion import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.getAvailableConcepts import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.getAvailableOperations import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toObject @@ -41,10 +42,10 @@ import io.modelcontextprotocol.kotlin.sdk.types.Role import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonObject +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.* + +@Serializable data class OverlaySuggestionsResponse(val items: List) fun Server.addCpgLlmAnalyzeTool() { val toolDescription = @@ -173,7 +174,7 @@ fun Server.addCpgLlmAnalyzeTool() { appendLine( """ { - "overlaySuggestions": [ + "items": [ { "nodeId": "1234", "overlay": "fully.qualified.class.Name", @@ -193,7 +194,7 @@ fun Server.addCpgLlmAnalyzeTool() { ) } - // Get the session ID (assuming single session setup) + // Get the session ID val sessionId = this.sessions.values.firstOrNull()?.sessionId if (sessionId == null) { @@ -228,10 +229,37 @@ fun Server.addCpgLlmAnalyzeTool() { // Send sampling request to client (which will forward to LLM) val result = this.createMessage(sessionId, samplingRequest) - // Extract the text content from the result + // Extract the LLM response val llmResponse = (result.content as? TextContent)?.text ?: "No response from LLM" - CallToolResult(content = listOf(TextContent(llmResponse))) + // Extract and validate JSON + val jsonStr = extractJson(llmResponse) + + // Parse LLM response and enrich with further node properties + try { + val parsed = Json.decodeFromString(jsonStr) + val nodes = globalAnalysisResult?.nodes ?: emptyList() + + val enrichedItems = + parsed.items.map { suggestion -> + val node = nodes.find { it.id.toString() == suggestion.nodeId } + val conceptNode = + suggestion.conceptNodeId?.let { id -> + nodes.find { it.id.toString() == id } + } + + suggestion.copy( + node = node?.toJSON(noEdges = true), + conceptNode = conceptNode?.toJSON(noEdges = true), + ) + } + + val response = OverlaySuggestionsResponse(items = enrichedItems) + CallToolResult(content = listOf(TextContent(Json.encodeToString(response)))) + } catch (_: Exception) { + // If parsing fails, return the extracted JSON anyway + CallToolResult(content = listOf(TextContent(jsonStr))) + } } } catch (e: Exception) { CallToolResult( @@ -243,3 +271,26 @@ fun Server.addCpgLlmAnalyzeTool() { } } } + +/** + * Extract JSON from LLM response that might be wrapped in markdown code blocks or contain extra + * text + */ +private fun extractJson(response: String): String { + val trimmed = response.trim() + + val fencedMatch = Regex("(?s)```(?:json)?\\s*(\\{.*?\\})\\s*```").find(trimmed) + if (fencedMatch != null) { + return fencedMatch.groupValues[1] + } + + // Try to extract JSON between first { and last } + val firstBrace = trimmed.indexOf('{') + val lastBrace = trimmed.lastIndexOf('}') + if (firstBrace >= 0 && lastBrace > firstBrace) { + return trimmed.substring(firstBrace, lastBrace + 1) + } + + // Return as-is if no extraction worked + return trimmed +} diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt index 4be53f2a513..ae9cb521e77 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt @@ -25,6 +25,7 @@ */ package de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils +import de.fraunhofer.aisec.cpg.serialization.NodeJSON import kotlinx.serialization.Serializable @Serializable @@ -41,8 +42,6 @@ data class CpgCallArgumentByNameOrIndexPayload( val index: Int? = null, ) -@Serializable data class CpgApplyConceptsPayload(val assignments: List) - @Serializable data class CpgRunPassPayload( /** The FQN of the pass to run. */ @@ -52,19 +51,20 @@ data class CpgRunPassPayload( ) @Serializable -data class ConceptAssignment( +data class OverlaySuggestion( val nodeId: String, - /* FQN of concept or operation class */ val overlay: String, - /* "Concept" or "Operation" from LLM response */ val overlayType: String? = null, - /* NodeId of concept this operation references */ val conceptNodeId: String? = null, val arguments: Map? = null, val reasoning: String? = null, val securityImpact: String? = null, + val node: NodeJSON? = null, + val conceptNode: NodeJSON? = null, ) +@Serializable data class CpgApplyConceptsPayload(val items: List) + @Serializable data class CpgDataflowPayload(val from: String, val to: String) @Serializable data class CpgLlmAnalyzePayload(val description: String? = null) From 5522b1bb930d266155970eb4ba3de7ae364eb45f Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 26 Jan 2026 14:10:56 +0100 Subject: [PATCH 49/93] Refactor clients --- .../ai/{McpClient.kt => ChatClient.kt} | 87 ++++++------------- .../aisec/codyze/console/ai/ChatService.kt | 42 +++++---- .../codyze/console/ai/clients/GeminiClient.kt | 35 ++++---- .../codyze/console/ai/clients/LlmClient.kt | 55 ++++++++++++ .../codyze/console/ai/clients/OpenAiClient.kt | 87 +++++++++---------- .../aisec/codyze/console/ai/clients/Util.kt | 63 +++++++------- .../src/main/resources/application.conf | 13 ++- .../components/ai-agent/ChatInterface.svelte | 2 +- .../ai-agent/widgets/ToolResultWidget.svelte | 67 +------------- .../ai-agent/widgets/widgetRegistry.ts | 1 - .../src/main/webapp/src/lib/types.ts | 3 +- .../webapp/src/routes/ai-agent/+page.svelte | 76 +++------------- 12 files changed, 214 insertions(+), 317 deletions(-) rename codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/{McpClient.kt => ChatClient.kt} (68%) create mode 100644 codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt similarity index 68% rename from codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpClient.kt rename to codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt index e800094235f..bb657306c84 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt @@ -25,11 +25,7 @@ */ package de.fraunhofer.aisec.codyze.console.ai -import de.fraunhofer.aisec.codyze.console.ai.clients.Events -import de.fraunhofer.aisec.codyze.console.ai.clients.GeminiClient -import de.fraunhofer.aisec.codyze.console.ai.clients.OpenAiClient -import de.fraunhofer.aisec.codyze.console.ai.clients.ToolCall -import de.fraunhofer.aisec.codyze.console.ai.clients.ToolCallWithResult +import de.fraunhofer.aisec.codyze.console.ai.clients.* import io.ktor.client.* import io.modelcontextprotocol.kotlin.sdk.client.Client as McpSdkClient import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions @@ -39,11 +35,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.serialization.json.* -class McpClient( +class ChatClient( private val httpClient: HttpClient, - private val geminiClient: GeminiClient?, - private val openAiClient: OpenAiClient?, - private val llmModel: String, + private val llm: LlmClient, private val mcpServerUrl: String, ) { @@ -72,32 +66,22 @@ class McpClient( _ -> try { val llmResponse = - when { - geminiClient != null -> - geminiClient.query( - messages = request.params.messages, - systemPrompt = request.params.systemPrompt, - ) - openAiClient != null -> - openAiClient.query( - messages = request.params.messages, - systemPrompt = request.params.systemPrompt, - maxTokens = request.params.maxTokens, - ) - else -> throw IllegalStateException("No LLM client configured") - } + llm.query( + messages = request.params.messages, + systemPrompt = request.params.systemPrompt, + ) CreateMessageResult( role = Role.Assistant, content = TextContent(text = llmResponse), - model = llmModel, + model = llm.modelName, stopReason = StopReason.EndTurn, ) } catch (e: Exception) { CreateMessageResult( role = Role.Assistant, content = TextContent(text = "Error: ${e.message}"), - model = llmModel, + model = llm.modelName, stopReason = StopReason.EndTurn, ) } @@ -106,69 +90,48 @@ class McpClient( /** Process a chat query using the LLM with MCP tool support */ fun chat(request: ChatRequestJSON): Flow = channelFlow { - // Send keepalive to prevent browser timeout - // TODO: Why is this needed even though the HTTP client has a timeout configured? - // Apparently, it is only needed when prompting our local models send(Events.keepalive()) val userMessage = request.messages.lastOrNull()?.content ?: "" val conversationHistory = request.messages try { - // First call: check for tool calls, stream everything val toolCalls = - sendPrompt(userMessage, conversationHistory = conversationHistory, tools = tools) { - event -> - // Stream all events (reasoning and text) directly - send(event) - } + llm.sendPrompt( + userMessage = userMessage, + conversationHistory = conversationHistory, + tools = tools, + onText = { text -> send(Events.text(text)) }, + onReasoning = { thought -> send(Events.reasoning(thought)) }, + ) if (toolCalls.isNotEmpty()) { val toolResults = toolCalls.map { toolCall -> - val result = executeToolCall(toolCall) { event -> send(event) } + val result = executeToolCall(toolCall) { jsonEvent -> send(jsonEvent) } ToolCallWithResult(toolCall, result) } - sendPrompt( - userMessage, + llm.sendPrompt( + userMessage = userMessage, conversationHistory = conversationHistory, toolResults = toolResults, - ) { event -> - send(event) - } + onText = { text -> send(Events.text(text)) }, + onReasoning = { thought -> send(Events.reasoning(thought)) }, + ) } } catch (e: Exception) { + println("[LLM] Error: ${e.message}") send(Events.text("Error: ${e.message}")) } } - /** Send a prompt to the configured LLM */ - private suspend fun sendPrompt( - userMessage: String, - conversationHistory: List = emptyList(), - tools: List = emptyList(), - toolResults: List? = null, - emit: suspend (String) -> Unit, - ): List { - return when { - geminiClient != null -> - geminiClient.sendPrompt(userMessage, conversationHistory, tools, toolResults, emit) - openAiClient != null -> - openAiClient.sendPrompt(userMessage, conversationHistory, tools, toolResults, emit) - else -> throw IllegalStateException("No LLM client configured") - } - } - /** Execute a tool call and emit result to frontend */ private suspend fun executeToolCall( toolCall: ToolCall, emit: suspend (String) -> Unit, ): String { return try { - // Emit pending state so frontend can show loading indicator - emit(Events.toolPending(toolCall)) - val jsonArgs = Json.parseToJsonElement(toolCall.arguments).jsonObject val arguments: Map = jsonArgs.toMap() @@ -177,7 +140,9 @@ class McpClient( val resultText = contentTexts.joinToString("\n") val content = buildToolContentPayload(contentTexts) - emit(Events.toolResult(toolCall.name, content)) + val event = Events.toolResult(toolCall.name, content) + println("[Tool] Emitting event: $event") + emit(event) resultText } catch (e: Exception) { diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt index 7d49760002d..33cba53ab03 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt @@ -27,6 +27,7 @@ package de.fraunhofer.aisec.codyze.console.ai import com.typesafe.config.ConfigFactory import de.fraunhofer.aisec.codyze.console.ai.clients.GeminiClient +import de.fraunhofer.aisec.codyze.console.ai.clients.LlmClient import de.fraunhofer.aisec.codyze.console.ai.clients.OpenAiClient import io.ktor.client.* import io.ktor.client.engine.cio.* @@ -45,10 +46,10 @@ import kotlinx.serialization.json.Json /** ChatService manages LLM client configuration and provides an API for chat interactions. */ class ChatService { private val config = ConfigFactory.load() - private val llmClient: String = config.getString("llm.client") + private val llmProvider: String = config.getString("llm.client") private val llmModel: String = - when (llmClient) { + when (llmProvider) { "ollama" -> config.getString("llm.ollama.model") "vLLM" -> config.getString("llm.vLLM.model") "gemini" -> config.getString("llm.gemini.model") @@ -73,36 +74,33 @@ class ChatService { } } - val geminiClient: GeminiClient? = - if (llmClient == "gemini") { - val apiKey = - System.getenv("GOOGLE_API_KEY") - ?: throw IllegalStateException("GOOGLE_API_KEY not set") - GeminiClient(httpClient, llmModel, apiKey, config.getString("llm.gemini.baseUrl")) - } else null - - val openAiClient: OpenAiClient? = - when (llmClient) { - "ollama" -> OpenAiClient(httpClient, llmModel, config.getString("llm.ollama.baseUrl")) - "vLLM" -> OpenAiClient(httpClient, llmModel, config.getString("llm.vLLM.baseUrl")) - else -> null + private val llmClient: LlmClient = + when (llmProvider) { + "gemini" -> { + val apiKey = + System.getenv("GOOGLE_API_KEY") + ?: throw IllegalStateException("GOOGLE_API_KEY not set") + GeminiClient(httpClient, llmModel, apiKey, config.getString("llm.gemini.baseUrl")) + } + "ollama", + "vLLM", + "local" -> OpenAiClient(httpClient, llmModel, config.getString("llm.ollama.baseUrl")) + else -> throw IllegalArgumentException("Unsupported LLM provider: $llmProvider") } - private val mcpClient: McpClient = - McpClient( + private val chatClient: ChatClient = + ChatClient( httpClient = httpClient, - geminiClient = geminiClient, - openAiClient = openAiClient, - llmModel = llmModel, + llm = llmClient, mcpServerUrl = config.getString("mcp.serverUrl"), ) suspend fun connect() { - mcpClient.connect() + chatClient.connect() } fun chat(request: ChatRequestJSON): Flow { - return mcpClient.chat(request) + return chatClient.chat(request) } fun close() { diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt index 37751425876..8dbe2b263f5 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt @@ -25,6 +25,7 @@ */ package de.fraunhofer.aisec.codyze.console.ai.clients +import de.fraunhofer.aisec.codyze.console.ai.ChatMessageJSON import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* @@ -40,8 +41,14 @@ class GeminiClient( private val model: String, private val apiKey: String, private val baseUrl: String, -) { - suspend fun query(messages: List, systemPrompt: String?): String { +) : LlmClient { + override val modelName: String = model + + override suspend fun query( + messages: List, + systemPrompt: String?, + maxTokens: Int?, + ): String { val request = GeminiRequest( systemInstruction = @@ -67,17 +74,16 @@ class GeminiClient( return extractTextFromResponse(result) ?: "No response" } - /** Send a prompt to Gemini and stream the response. */ - suspend fun sendPrompt( + override suspend fun sendPrompt( userMessage: String, - conversationHistory: List = emptyList(), - tools: List = emptyList(), - toolResults: List? = null, - emit: suspend (String) -> Unit, + conversationHistory: List, + tools: List, + toolResults: List?, + onText: suspend (String) -> Unit, + onReasoning: suspend (String) -> Unit, ): List { val toolCalls = mutableListOf() - // Only include tools if we're not already processing tool results val geminiTools = if (toolResults == null && tools.isNotEmpty()) { listOf( @@ -101,7 +107,6 @@ class GeminiClient( ) } else null - // Build contents based on whether we have tool results val contents = if (toolResults != null) { listOf( @@ -154,20 +159,20 @@ class GeminiClient( .execute { response -> if (response.status.value >= 400) { val errorBody = response.body() - emit(Events.text("Gemini API error (${response.status}): $errorBody")) + onText("Gemini API error (${response.status}): $errorBody") return@execute } val channel = response.body() - processStream(channel, emit, toolCalls) + streamMessages(channel, onText, toolCalls) } return toolCalls } - private suspend fun processStream( + private suspend fun streamMessages( channel: ByteReadChannel, - emit: suspend (String) -> Unit, + onText: suspend (String) -> Unit, toolCalls: MutableList, ) { readSseStream(channel) { jsonStr -> @@ -186,7 +191,7 @@ class GeminiClient( val partObj = part.jsonObject partObj["text"]?.jsonPrimitive?.contentOrNull?.let { text -> - if (text.isNotEmpty()) emit(Events.text(text)) + if (text.isNotEmpty()) onText(text) } partObj["functionCall"]?.jsonObject?.let { fc -> diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt new file mode 100644 index 00000000000..c5af73a80bb --- /dev/null +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026, 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.console.ai.clients + +import de.fraunhofer.aisec.codyze.console.ai.ChatMessageJSON +import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage +import io.modelcontextprotocol.kotlin.sdk.types.Tool + +/** Interface abstracting the underlying LLM provider (Gemini, OpenAI, Ollama, etc.). */ +interface LlmClient { + val modelName: String + + /** Query for MCP sampling requests. */ + suspend fun query( + messages: List, + systemPrompt: String?, + maxTokens: Int? = null, + ): String + + /** + * Streaming prompt execution for the chat. Calls [onText] for normal content and [onReasoning] + * for thoughts/reasoning. + */ + suspend fun sendPrompt( + userMessage: String, + conversationHistory: List = emptyList(), + tools: List = emptyList(), + toolResults: List? = null, + onText: suspend (String) -> Unit, + onReasoning: suspend (String) -> Unit = {}, + ): List +} diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt index f6ff89163cd..f6e2c296ada 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt @@ -25,6 +25,7 @@ */ package de.fraunhofer.aisec.codyze.console.ai.clients +import de.fraunhofer.aisec.codyze.console.ai.ChatMessageJSON import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* @@ -40,14 +41,15 @@ class OpenAiClient( private val httpClient: HttpClient, private val model: String, private val baseUrl: String, -) { +) : LlmClient { + override val modelName: String = model private val usesThinkTags = model.contains("glm", ignoreCase = true) - /** Query (non-streaming) for sampling requests */ - suspend fun query( + /** Query the LLM when we have a tool that uses sampling */ + override suspend fun query( messages: List, systemPrompt: String?, - maxTokens: Int, + maxTokens: Int?, ): String { val openAiMessages = buildList { if (systemPrompt != null) { @@ -85,36 +87,26 @@ class OpenAiClient( return extractContentFromResponse(result) ?: "No response" } - /** Send a prompt to the LLM and stream the response. */ - suspend fun sendPrompt( + override suspend fun sendPrompt( userMessage: String, - conversationHistory: List = emptyList(), - tools: List = emptyList(), - toolResults: List? = null, - emit: suspend (String) -> Unit, + conversationHistory: List, + tools: List, + toolResults: List?, + onText: suspend (String) -> Unit, + onReasoning: suspend (String) -> Unit, ): List { val collectedToolCalls = mutableListOf() val accumulatedToolCalls = mutableListOf() - // Build messages based on conversation history and tool results val messages = buildList { add(OpenAiMessage(role = "system", content = JsonPrimitive(SYSTEM_PROMPT))) - // Add conversation history conversationHistory.dropLast(1).forEach { msg -> - try { - val msgJson = Json.parseToJsonElement(Json.encodeToString(msg)).jsonObject - val role = msgJson["role"]?.jsonPrimitive?.contentOrNull ?: "user" - val content = msgJson["content"]?.jsonPrimitive?.contentOrNull ?: "" - if (role == "user" && content.isNotBlank()) { - add(OpenAiMessage(role = role, content = JsonPrimitive(content))) - } - } catch (_: Exception) { - // Skip messages + if (msg.role == "user" && msg.content.isNotBlank()) { + add(OpenAiMessage(role = msg.role, content = JsonPrimitive(msg.content))) } } - // Add current user message add(OpenAiMessage(role = "user", content = JsonPrimitive(userMessage))) if (toolResults != null) { @@ -148,7 +140,6 @@ class OpenAiClient( } } - // Build tools if available and not already processing tool results val openAiTools = if (toolResults == null && tools.isNotEmpty()) { tools.map { tool -> @@ -165,10 +156,10 @@ class OpenAiClient( "properties", tool.inputSchema.properties ?: buildJsonObject {}, ) - tool.inputSchema.required?.let { + tool.inputSchema.required?.let { required -> put( "required", - JsonArray(it.map { r -> JsonPrimitive(r) }), + JsonArray(required.map { r -> JsonPrimitive(r) }), ) } }, @@ -180,6 +171,18 @@ class OpenAiClient( val request = OpenAiRequest(model = model, messages = messages, tools = openAiTools, stream = true) + // Token estimation: messages + tools + val msgChars = messages.sumOf { (it.content as? JsonPrimitive)?.content?.length ?: 0 } + val toolChars = + openAiTools?.sumOf { it.function.description.length + it.function.name.length } ?: 0 + val systemChars = SYSTEM_PROMPT.length + val totalChars = msgChars + toolChars + systemChars + println( + "[OpenAI] Sending request | Messages: ${messages.size} | Tools: ${openAiTools?.size ?: 0} | ~${totalChars / 4} tokens" + ) + val requestJson = Json { prettyPrint = true }.encodeToString(request) + println("[OpenAI] Request:\n$requestJson") + httpClient .preparePost("$baseUrl/v1/chat/completions") { contentType(ContentType.Application.Json) @@ -188,17 +191,13 @@ class OpenAiClient( .execute { response -> if (!response.status.isSuccess()) { val errorBody = response.body() - val errorMsg = - "LLM request failed: ${response.status.value} ${response.status.description}\n$errorBody" - println("ERROR: $errorMsg") - emit(Events.text(errorMsg)) + onText("LLM request failed: ${response.status.value}\n$errorBody") return@execute } val channel = response.body() - processStream(channel, emit, accumulatedToolCalls) + streamMessages(channel, onText, onReasoning, accumulatedToolCalls) } - // Convert accumulated tool calls to ToolCall objects for (tcObj in accumulatedToolCalls) { val function = tcObj["function"]?.jsonObject val name = function?.get("name")?.jsonPrimitive?.contentOrNull @@ -211,9 +210,10 @@ class OpenAiClient( return collectedToolCalls } - private suspend fun processStream( + private suspend fun streamMessages( channel: ByteReadChannel, - emit: suspend (String) -> Unit, + onText: suspend (String) -> Unit, + onReasoning: suspend (String) -> Unit, accumulatedToolCalls: MutableList, ) { val toolCallsMap = mutableMapOf>() @@ -226,34 +226,32 @@ class OpenAiClient( chunk["choices"]?.jsonArray?.firstOrNull()?.jsonObject?.get("delta")?.jsonObject if (delta != null) { - // Try different reasoning fields that different models might use val reasoningContent = delta["reasoning"]?.jsonPrimitive?.contentOrNull ?: delta["thoughts"]?.jsonPrimitive?.contentOrNull ?: delta["thinking"]?.jsonPrimitive?.contentOrNull if (reasoningContent?.isNotEmpty() == true) { - emit(Events.reasoning(reasoningContent)) + onReasoning(reasoningContent) } - // Regular content delta["content"]?.jsonPrimitive?.contentOrNull?.let { content -> if (content.isNotEmpty()) { if (usesThinkTags) { seenThinkClose = - processThinkTagsStreaming( + thinkTagsStreaming( content, reasoningBuffer, seenThinkClose, - emit, + onText, + onReasoning, ) } else { - emit(Events.text(content)) + onText(content) } } } - // Tool calls delta["tool_calls"]?.jsonArray?.forEach { tcElement -> val tc = tcElement.jsonObject val index = tc["index"]?.jsonPrimitive?.intOrNull ?: 0 @@ -279,7 +277,6 @@ class OpenAiClient( } } - // Convert accumulated tool calls to JsonObjects toolCallsMap.values.forEach { toolCall -> if (toolCall["name"] != null) { accumulatedToolCalls.add( @@ -311,11 +308,7 @@ class OpenAiClient( ?.jsonPrimitive ?.content - return processContentForModel(content) - } - - private fun processContentForModel(content: String?): String? { - if (content == null) return null - return if (usesThinkTags) stripThinkTags(content) else content + return if (content == null) null + else if (usesThinkTags) stripThinkTags(content) else content } } diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt index f2c5922fc4e..7b4caa57593 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt @@ -26,10 +26,10 @@ package de.fraunhofer.aisec.codyze.console.ai.clients import io.ktor.utils.io.* -import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put const val SYSTEM_PROMPT = "You are a code analysis assistant with access to CPG (Code Property Graph) tools. " + @@ -59,12 +59,13 @@ suspend fun readSseStream(channel: ByteReadChannel, processLine: suspend (String } } -/** Process tags during streaming - updates seenThinkClose via mutable parameter */ -suspend fun processThinkTagsStreaming( +/** LLMs like GLM use tags instead of the common approach using tags */ +suspend fun thinkTagsStreaming( content: String, reasoningBuffer: StringBuilder, seenThinkClose: Boolean, - emit: suspend (String) -> Unit, + onText: suspend (String) -> Unit, + onReasoning: suspend (String) -> Unit, ): Boolean { var closed = seenThinkClose @@ -73,7 +74,7 @@ suspend fun processThinkTagsStreaming( reasoningBuffer.append(beforeClose) if (reasoningBuffer.isNotEmpty()) { - emit(Events.reasoning(reasoningBuffer.toString())) + onReasoning(reasoningBuffer.toString()) reasoningBuffer.clear() } @@ -81,13 +82,13 @@ suspend fun processThinkTagsStreaming( val afterClose = content.substringAfter("") if (afterClose.isNotEmpty()) { - emit(Events.text(afterClose)) + onText(afterClose) } } else { if (!closed) { reasoningBuffer.append(content) } else { - emit(Events.text(content)) + onText(content) } } @@ -99,36 +100,32 @@ fun stripThinkTags(content: String): String { return content.replace(Regex(".*?", RegexOption.DOT_MATCHES_ALL), "").trim() } -/** Stream events sent to frontend */ -@Serializable -data class StreamEvent( - val type: String, - val content: String? = null, - val toolName: String? = null, - val arguments: String? = null, - val toolContent: JsonElement? = null, -) - -/** Helper to create stream events */ +/** Helper to create stream events for the frontend */ object Events { - fun text(content: String) = Json.encodeToString(StreamEvent(type = "text", content = content)) - - fun reasoning(content: String) = - Json.encodeToString(StreamEvent(type = "reasoning", content = content)) - - fun keepalive() = Json.encodeToString(StreamEvent(type = "keepalive")) + fun text(content: String): String = + Json.encodeToString( + buildJsonObject { + put("type", "text") + put("content", content) + } + ) - fun toolPending(toolCall: ToolCall) = + fun reasoning(content: String): String = Json.encodeToString( - StreamEvent( - type = "tool_pending", - toolName = toolCall.name, - arguments = toolCall.arguments, - ) + buildJsonObject { + put("type", "reasoning") + put("content", content) + } ) - fun toolResult(toolName: String, content: JsonElement) = + fun keepalive(): String = Json.encodeToString(buildJsonObject { put("type", "keepalive") }) + + fun toolResult(toolName: String, content: JsonElement): String = Json.encodeToString( - StreamEvent(type = "tool_result", toolName = toolName, toolContent = content) + buildJsonObject { + put("type", "tool_result") + put("toolName", toolName) + put("content", content) + } ) } diff --git a/codyze-console/src/main/resources/application.conf b/codyze-console/src/main/resources/application.conf index c7d34096641..23598f28ef2 100644 --- a/codyze-console/src/main/resources/application.conf +++ b/codyze-console/src/main/resources/application.conf @@ -6,10 +6,15 @@ llm { model = "gemini-2.5-flash" } - ollama { - baseUrl = "http://192.168.178.59:1234" #"http://172.31.6.131:11434" - model = "zai-org/glm-4.7-flash" # gpt-oss:120b - } + ollama { + baseUrl = "http://172.31.6.131:11434" + model = "qwen3-next:latest" #"gpt-oss:120b" + } + + local { + baseUrl = "http://192.168.178.59:1234" + model = "zai-org/glm-4.7-flash" + } vLLM { baseUrl = "http://172.31.6.131:8000/v1/chat/completions" diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte index de0b18f2778..1d1679db75b 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte @@ -199,7 +199,7 @@
{/if} - {#if (message.contentType === 'tool-result' || message.contentType === 'tool-pending') && message.toolResult} + {#if message.contentType === 'tool-result' && message.toolResult} {:else if message.content} diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte index 3654b8524d8..f536558cb5f 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte @@ -59,19 +59,7 @@
- {#if data.isPending} - -
-
-
- {data.toolName} - Running -
-
- Executing tool... -
-
- {:else if selectedWidget} + {#if selectedWidget} {@const SelectedWidget = selectedWidget} @@ -141,57 +129,4 @@ white-space: pre-wrap; word-wrap: break-word; } - - /* Pending state styles */ - .pending-widget { - margin: 0.5rem 0; - background: white; - border: 1px solid #e5e7eb; - border-radius: 0.5rem; - overflow: hidden; - } - - .pending-header { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - background: #f0f9ff; - border-bottom: 1px solid #e0f2fe; - } - - .pending-spinner { - width: 1rem; - height: 1rem; - border: 2px solid #bfdbfe; - border-top-color: #3b82f6; - border-radius: 50%; - animation: spin 0.8s linear infinite; - } - - @keyframes spin { - to { - transform: rotate(360deg); - } - } - - .pending-badge { - padding: 0.25rem 0.5rem; - background: #dbeafe; - color: #1d4ed8; - font-size: 0.75rem; - font-weight: 600; - border-radius: 0.25rem; - text-transform: uppercase; - } - - .pending-content { - padding: 1rem; - } - - .pending-text { - color: #6b7280; - font-size: 0.875rem; - font-style: italic; - } diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts index 2279d2c18f6..6a8e31328c1 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts @@ -4,7 +4,6 @@ export type ToolResultData = { toolName?: string; content: any; isError?: boolean; - isPending?: boolean; }; export type WidgetProps = { diff --git a/codyze-console/src/main/webapp/src/lib/types.ts b/codyze-console/src/main/webapp/src/lib/types.ts index b3b3521aaf9..8bd061fc3ce 100644 --- a/codyze-console/src/main/webapp/src/lib/types.ts +++ b/codyze-console/src/main/webapp/src/lib/types.ts @@ -79,14 +79,13 @@ export interface ToolResult { toolName?: string; content: any; isError?: boolean; - isPending?: boolean; } export interface ChatMessage { id: string; role: 'user' | 'assistant'; content: string; - contentType?: 'text' | 'tool-result' | 'tool-pending'; + contentType?: 'text' | 'tool-result'; toolResult?: ToolResult; reasoning?: string; timestamp: Date; diff --git a/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte index b5485170dec..0d0138ff21d 100644 --- a/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte +++ b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte @@ -39,8 +39,6 @@ let streamingContent = $state(''); let streamingReasoning = $state(''); let showWelcome = $state(persisted.showWelcome); - // Track pending tools by name for loading state - let pendingTools = $state>(new Map()); // Save state to sessionStorage whenever it changes $effect(() => { @@ -65,7 +63,6 @@ currentMessage = ''; streamingContent = ''; streamingReasoning = ''; - pendingTools = new Map(); } async function sendMessage() { @@ -97,66 +94,21 @@ const event = JSON.parse(chunk); if (event.type === 'text') { - // Normal text streaming streamingContent += event.content; } else if (event.type === 'reasoning') { - // LLM thinking/reasoning content - accumulate it streamingReasoning += event.content; - } else if (event.type === 'tool_pending') { - // Tool is starting - add a pending message immediately - const pendingMessage: ChatMessage = { - id: `pending-${event.toolName}-${Date.now()}`, + } else if (event.type === 'tool_result') { + chatMessages = [...chatMessages, { + id: Date.now().toString(), role: 'assistant' as const, content: '', - contentType: 'tool-pending', + contentType: 'tool-result', toolResult: { toolName: event.toolName, - content: null, - isPending: true + content: event.content }, timestamp: new Date() - }; - chatMessages = [...chatMessages, pendingMessage]; - // Track this pending tool - pendingTools.set(event.toolName, { toolName: event.toolName, arguments: event.arguments }); - } else if (event.type === 'tool_result') { - // Tool completed - replace pending message with result - console.log('Received tool_result event:', event); - // Find and update the pending message for this tool - const pendingIdx = chatMessages.findIndex( - m => m.contentType === 'tool-pending' && m.toolResult?.toolName === event.toolName - ); - if (pendingIdx !== -1) { - // Replace pending with actual result - const updatedMessages = [...chatMessages]; - updatedMessages[pendingIdx] = { - ...updatedMessages[pendingIdx], - id: Date.now().toString(), - contentType: 'tool-result', - toolResult: { - toolName: event.toolName, - content: event.content, - isPending: false - } - }; - chatMessages = updatedMessages; - } else { - // No pending message found, add as new - chatMessages = [...chatMessages, { - id: Date.now().toString(), - role: 'assistant' as const, - content: '', - contentType: 'tool-result', - toolResult: { - toolName: event.toolName, - content: event.content, - isPending: false - }, - timestamp: new Date() - }]; - } - // Remove from pending tracking - pendingTools.delete(event.toolName); + }]; } } catch (e) { // If JSON parsing fails, treat as plain text @@ -177,13 +129,11 @@ streamingContent = ''; }, onComplete: () => { - // Capture reasoning and attach to first tool message if exists const reasoning = streamingReasoning || undefined; + + // Attach reasoning to first tool-result message if exists if (reasoning) { - // Find first tool-result message and attach reasoning - const firstToolIdx = chatMessages.findIndex( - m => m.contentType === 'tool-result' || m.contentType === 'tool-pending' - ); + const firstToolIdx = chatMessages.findIndex(m => m.contentType === 'tool-result'); if (firstToolIdx !== -1 && !chatMessages[firstToolIdx].reasoning) { const updatedMessages = [...chatMessages]; updatedMessages[firstToolIdx] = { @@ -194,18 +144,15 @@ } } - // Add text response (LLM summary) if present + // Add text response if present const content = streamingContent; if (content && content.trim().length > 0) { - const hasToolResults = chatMessages.some( - m => m.contentType === 'tool-result' || m.contentType === 'tool-pending' - ); + const hasToolResults = chatMessages.some(m => m.contentType === 'tool-result'); chatMessages = [...chatMessages, { id: (Date.now() + 1).toString(), role: 'assistant' as const, content: content, contentType: 'text', - // Attach reasoning to text only if no tool results reasoning: !hasToolResults ? reasoning : undefined, timestamp: new Date() }]; @@ -215,7 +162,6 @@ isLoading = false; streamingContent = ''; streamingReasoning = ''; - pendingTools = new Map(); } }; From 741b34bc543ec150b385ae7ef8945a39b283b3f1 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 27 Jan 2026 15:29:42 +0100 Subject: [PATCH 50/93] Add logs for token usage --- .../aisec/codyze/console/ai/clients/OpenAiClient.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt index f6e2c296ada..aecb0375a16 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt @@ -219,6 +219,7 @@ class OpenAiClient( val toolCallsMap = mutableMapOf>() val reasoningBuffer = StringBuilder() var seenThinkClose = false + var totalOutputChars = 0 readSseStream(channel) { jsonStr -> val chunk = Json.parseToJsonElement(jsonStr).jsonObject @@ -232,10 +233,12 @@ class OpenAiClient( ?: delta["thinking"]?.jsonPrimitive?.contentOrNull if (reasoningContent?.isNotEmpty() == true) { + totalOutputChars += reasoningContent.length onReasoning(reasoningContent) } delta["content"]?.jsonPrimitive?.contentOrNull?.let { content -> + totalOutputChars += content.length if (content.isNotEmpty()) { if (usesThinkTags) { seenThinkClose = @@ -268,15 +271,21 @@ class OpenAiClient( tc["id"]?.jsonPrimitive?.contentOrNull?.let { toolCall["id"] = it } tc["type"]?.jsonPrimitive?.contentOrNull?.let { toolCall["type"] = it } tc["function"]?.jsonObject?.let { func -> - func["name"]?.jsonPrimitive?.contentOrNull?.let { toolCall["name"] = it } + func["name"]?.jsonPrimitive?.contentOrNull?.let { + toolCall["name"] = it + totalOutputChars += it.length + } func["arguments"]?.jsonPrimitive?.contentOrNull?.let { args -> toolCall["arguments"] = (toolCall["arguments"] as String) + args + totalOutputChars += args.length } } } } } + println("Response finished | ~${totalOutputChars / 4} tokens") + toolCallsMap.values.forEach { toolCall -> if (toolCall["name"] != null) { accumulatedToolCalls.add( From 53025262e83813eb69d6fb330ebc02fa827947e6 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 29 Jan 2026 09:39:53 +0100 Subject: [PATCH 51/93] Change model --- .../aisec/codyze/console/ai/clients/OpenAiClient.kt | 4 ++-- codyze-console/src/main/resources/application.conf | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt index aecb0375a16..cec528fd2ed 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt @@ -178,10 +178,10 @@ class OpenAiClient( val systemChars = SYSTEM_PROMPT.length val totalChars = msgChars + toolChars + systemChars println( - "[OpenAI] Sending request | Messages: ${messages.size} | Tools: ${openAiTools?.size ?: 0} | ~${totalChars / 4} tokens" + "Sending request: Messages: ${messages.size} | Tools: ${openAiTools?.size ?: 0} | ~${totalChars / 4} tokens" ) val requestJson = Json { prettyPrint = true }.encodeToString(request) - println("[OpenAI] Request:\n$requestJson") + println("Request:\n$requestJson") httpClient .preparePost("$baseUrl/v1/chat/completions") { diff --git a/codyze-console/src/main/resources/application.conf b/codyze-console/src/main/resources/application.conf index 23598f28ef2..b85461fa681 100644 --- a/codyze-console/src/main/resources/application.conf +++ b/codyze-console/src/main/resources/application.conf @@ -8,7 +8,7 @@ llm { ollama { baseUrl = "http://172.31.6.131:11434" - model = "qwen3-next:latest" #"gpt-oss:120b" + model = "qwen3:235b" #"gpt-oss:120b" } local { From 664ad42b35496a09ed7724be0adbc4bcfe7f9655 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 29 Jan 2026 16:37:33 +0100 Subject: [PATCH 52/93] Refactor sending conversation history to LLM; Extend config for mlx llms --- .../aisec/codyze/console/ai/ChatService.kt | 17 ++-- .../codyze/console/ai/clients/GeminiClient.kt | 81 +++++++++++-------- .../codyze/console/ai/clients/OpenAiClient.kt | 3 +- .../src/main/resources/application.conf | 11 ++- 4 files changed, 63 insertions(+), 49 deletions(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt index 33cba53ab03..809e74d910c 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt @@ -48,13 +48,9 @@ class ChatService { private val config = ConfigFactory.load() private val llmProvider: String = config.getString("llm.client") - private val llmModel: String = - when (llmProvider) { - "ollama" -> config.getString("llm.ollama.model") - "vLLM" -> config.getString("llm.vLLM.model") - "gemini" -> config.getString("llm.gemini.model") - else -> config.getString("llm.ollama.model") - } + private val llmModel: String = config.getString("llm.$llmProvider.model") + + private val llmBaseUrl: String = config.getString("llm.$llmProvider.baseUrl") private val httpClient = HttpClient(CIO) { @@ -80,12 +76,9 @@ class ChatService { val apiKey = System.getenv("GOOGLE_API_KEY") ?: throw IllegalStateException("GOOGLE_API_KEY not set") - GeminiClient(httpClient, llmModel, apiKey, config.getString("llm.gemini.baseUrl")) + GeminiClient(httpClient, llmModel, apiKey, llmBaseUrl) } - "ollama", - "vLLM", - "local" -> OpenAiClient(httpClient, llmModel, config.getString("llm.ollama.baseUrl")) - else -> throw IllegalArgumentException("Unsupported LLM provider: $llmProvider") + else -> OpenAiClient(httpClient, llmModel, llmBaseUrl) } private val chatClient: ChatClient = diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt index 8dbe2b263f5..27267a18dd9 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt @@ -107,41 +107,58 @@ class GeminiClient( ) } else null + val historyContents = buildList { + conversationHistory.dropLast(1).forEach { msg -> + if (msg.content.isNotBlank()) { + val role = if (msg.role == "assistant") "model" else "user" + add(GeminiContent(role = role, parts = listOf(GeminiPart(text = msg.content)))) + } + } + } + val contents = if (toolResults != null) { - listOf( - GeminiContent(role = "user", parts = listOf(GeminiPart(text = userMessage))), - GeminiContent( - role = "model", - parts = - toolResults.map { tr -> - GeminiPart( - functionCall = - GeminiFunctionCall( - name = tr.call.name, - args = - Json.parseToJsonElement(tr.call.arguments) - .jsonObject, - ) - ) - }, - ), - GeminiContent( - role = "user", - parts = - toolResults.map { tr -> - GeminiPart( - functionResponse = - GeminiFunctionResponse( - name = tr.call.name, - response = buildJsonObject { put("result", tr.result) }, - ) - ) - }, - ), - ) + historyContents + + listOf( + GeminiContent( + role = "user", + parts = listOf(GeminiPart(text = userMessage)), + ), + GeminiContent( + role = "model", + parts = + toolResults.map { tr -> + GeminiPart( + functionCall = + GeminiFunctionCall( + name = tr.call.name, + args = + Json.parseToJsonElement(tr.call.arguments) + .jsonObject, + ) + ) + }, + ), + GeminiContent( + role = "user", + parts = + toolResults.map { tr -> + GeminiPart( + functionResponse = + GeminiFunctionResponse( + name = tr.call.name, + response = + buildJsonObject { put("result", tr.result) }, + ) + ) + }, + ), + ) } else { - listOf(GeminiContent(role = "user", parts = listOf(GeminiPart(text = userMessage)))) + historyContents + + listOf( + GeminiContent(role = "user", parts = listOf(GeminiPart(text = userMessage))) + ) } val request = diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt index cec528fd2ed..359cd2338e7 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt @@ -102,7 +102,7 @@ class OpenAiClient( add(OpenAiMessage(role = "system", content = JsonPrimitive(SYSTEM_PROMPT))) conversationHistory.dropLast(1).forEach { msg -> - if (msg.role == "user" && msg.content.isNotBlank()) { + if (msg.content.isNotBlank()) { add(OpenAiMessage(role = msg.role, content = JsonPrimitive(msg.content))) } } @@ -171,7 +171,6 @@ class OpenAiClient( val request = OpenAiRequest(model = model, messages = messages, tools = openAiTools, stream = true) - // Token estimation: messages + tools val msgChars = messages.sumOf { (it.content as? JsonPrimitive)?.content?.length ?: 0 } val toolChars = openAiTools?.sumOf { it.function.description.length + it.function.name.length } ?: 0 diff --git a/codyze-console/src/main/resources/application.conf b/codyze-console/src/main/resources/application.conf index b85461fa681..e3dd74d5e72 100644 --- a/codyze-console/src/main/resources/application.conf +++ b/codyze-console/src/main/resources/application.conf @@ -1,5 +1,5 @@ llm { - client = "ollama" # Options: "ollama", "gemini", "vLLM" + client = "mlx" # Options: "ollama", "gemini", "vLLM" gemini { baseUrl = "https://generativelanguage.googleapis.com/v1beta" @@ -8,7 +8,12 @@ llm { ollama { baseUrl = "http://172.31.6.131:11434" - model = "qwen3:235b" #"gpt-oss:120b" + model = "gpt-oss:120b" #"qwen3:235b" + } + + mlx { + baseUrl = "http://172.31.9.131:8000" + model = "mlx-community/Qwen3-Next-80B-A3B-Instruct-4bit" } local { @@ -17,7 +22,7 @@ llm { } vLLM { - baseUrl = "http://172.31.6.131:8000/v1/chat/completions" + baseUrl = "http://172.31.6.131:8000" model = "zai-org/GLM-4.5-Air-FP8" } } From 1d58ede5d13756d7226f8d36c5466b7c884e4c5b Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Wed, 11 Feb 2026 15:30:17 +0100 Subject: [PATCH 53/93] Some refactoring --- .../de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt | 2 +- codyze-console/src/main/resources/application.conf | 4 ++-- .../fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt index 7b4caa57593..6b705b11fa0 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt @@ -95,7 +95,7 @@ suspend fun thinkTagsStreaming( return closed } -/** Strip tags from non-streaming content */ +/** Strip tags from non-streaming content Note: Needed for GLM models */ fun stripThinkTags(content: String): String { return content.replace(Regex(".*?", RegexOption.DOT_MATCHES_ALL), "").trim() } diff --git a/codyze-console/src/main/resources/application.conf b/codyze-console/src/main/resources/application.conf index e3dd74d5e72..c4dea0b4c04 100644 --- a/codyze-console/src/main/resources/application.conf +++ b/codyze-console/src/main/resources/application.conf @@ -1,5 +1,5 @@ llm { - client = "mlx" # Options: "ollama", "gemini", "vLLM" + client = "ollama" # Options: "ollama", "gemini", "vLLM" gemini { baseUrl = "https://generativelanguage.googleapis.com/v1beta" @@ -8,7 +8,7 @@ llm { ollama { baseUrl = "http://172.31.6.131:11434" - model = "gpt-oss:120b" #"qwen3:235b" + model = "gpt-oss:120b" } mlx { diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt index 0115b511f3c..7d0f71017e8 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt @@ -26,6 +26,7 @@ package de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.Node import de.fraunhofer.aisec.cpg.graph.OverlayNode import de.fraunhofer.aisec.cpg.graph.concepts.Concept @@ -78,7 +79,10 @@ fun getAvailableOperations(): List> { } } -inline fun JsonObject.toObject() = Json.decodeFromString(Json.encodeToString(this)) +@PublishedApi internal val lenientJson = Json { ignoreUnknownKeys = true } + +inline fun JsonObject.toObject() = + lenientJson.decodeFromString(Json.encodeToString(this)) fun CallToolRequest.runOnCpg( query: BiFunction From a43cd1a7eeba34ba450a5e64d6e65f9f75280947 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 17 Feb 2026 13:56:41 +0100 Subject: [PATCH 54/93] Change listing tools returning summaries instead of complete node --- .../aisec/codyze/console/ai/ChatClient.kt | 32 +- .../aisec/codyze/console/ai/ChatService.kt | 4 +- .../codyze/console/ai/clients/GeminiClient.kt | 75 +- .../codyze/console/ai/clients/LlmClient.kt | 2 +- .../codyze/console/ai/clients/OpenAiClient.kt | 56 +- .../aisec/codyze/console/ai/clients/Util.kt | 8 +- .../src/main/resources/application.conf | 2 +- .../components/ai-agent/ChatInterface.svelte | 20 +- .../components/ai-agent/CodePreview.svelte | 2 +- ...mListWidget.svelte => CodeItemList.svelte} | 226 +++--- .../ai-agent/widgets/ToolResultWidget.svelte | 132 ---- .../ai-agent/widgets/widgetRegistry.ts | 41 -- .../lib/components/analysis/CodeViewer.svelte | 36 +- .../webapp/src/routes/ai-agent/+page.svelte | 7 + .../ghidra-export4851929095593266204.cpp | 651 ++++++++++++++++++ .../resources/example/src/module1/main.py | 6 - .../resources/example/src/module2/pkg/mod.py | 2 - .../src/third-party/mylib/library/lib.py | 2 - .../aisec/cpg/mcp/mcpserver/McpServer.kt | 19 +- .../cpg/mcp/mcpserver/tools/CpgAnalyzeTool.kt | 12 +- .../mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt | 24 +- .../cpg/mcp/mcpserver/tools/ListCommands.kt | 61 +- .../cpg/mcp/mcpserver/tools/utils/Payloads.kt | 9 +- .../mcp/mcpserver/tools/utils/Serializable.kt | 38 +- .../cpg/mcp/mcpserver/tools/utils/Util.kt | 40 ++ .../aisec/cpg/mcp/CpgAnalyzeToolTest.kt | 1 + .../cpg/serialization/NodeSerialization.kt | 17 +- 27 files changed, 1109 insertions(+), 416 deletions(-) rename codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/{ItemListWidget.svelte => CodeItemList.svelte} (70%) delete mode 100644 codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte create mode 100644 codyze-core/src/integrationTest/resources/example/src/module1/ghidra-export4851929095593266204.cpp delete mode 100644 codyze-core/src/integrationTest/resources/example/src/module1/main.py delete mode 100644 codyze-core/src/integrationTest/resources/example/src/module2/pkg/mod.py delete mode 100644 codyze-core/src/integrationTest/resources/example/src/third-party/mylib/library/lib.py diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt index bb657306c84..c0c3dedc61f 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt @@ -88,6 +88,9 @@ class ChatClient( } } + /** Maximum number of tool call iterations before forcing a text response. */ + private val maxToolIterations = 8 + /** Process a chat query using the LLM with MCP tool support */ fun chat(request: ChatRequestJSON): Flow = channelFlow { send(Events.keepalive()) @@ -96,7 +99,10 @@ class ChatClient( val conversationHistory = request.messages try { - val toolCalls = + val maxAgentSteps = mutableListOf>() + var counter = 0 + + var toolCalls = llm.sendPrompt( userMessage = userMessage, conversationHistory = conversationHistory, @@ -105,20 +111,24 @@ class ChatClient( onReasoning = { thought -> send(Events.reasoning(thought)) }, ) - if (toolCalls.isNotEmpty()) { - val toolResults = + while (toolCalls.isNotEmpty() && counter < maxToolIterations) { + counter++ + val roundtripResults = toolCalls.map { toolCall -> val result = executeToolCall(toolCall) { jsonEvent -> send(jsonEvent) } ToolCallWithResult(toolCall, result) } - - llm.sendPrompt( - userMessage = userMessage, - conversationHistory = conversationHistory, - toolResults = toolResults, - onText = { text -> send(Events.text(text)) }, - onReasoning = { thought -> send(Events.reasoning(thought)) }, - ) + maxAgentSteps.add(roundtripResults) + + toolCalls = + llm.sendPrompt( + userMessage = userMessage, + conversationHistory = conversationHistory, + maxAgentSteps = maxAgentSteps, + tools = tools, + onText = { text -> send(Events.text(text)) }, + onReasoning = { thought -> send(Events.reasoning(thought)) }, + ) } } catch (e: Exception) { println("[LLM] Error: ${e.message}") diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt index 809e74d910c..9b3d5c5fce3 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt @@ -74,8 +74,8 @@ class ChatService { when (llmProvider) { "gemini" -> { val apiKey = - System.getenv("GOOGLE_API_KEY") - ?: throw IllegalStateException("GOOGLE_API_KEY not set") + System.getenv("GEMINI_API_KEY") + ?: throw IllegalStateException("GEMINI_API_KEY not set") GeminiClient(httpClient, llmModel, apiKey, llmBaseUrl) } else -> OpenAiClient(httpClient, llmModel, llmBaseUrl) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt index 27267a18dd9..d7be305dabd 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt @@ -78,14 +78,14 @@ class GeminiClient( userMessage: String, conversationHistory: List, tools: List, - toolResults: List?, + maxAgentSteps: List>?, onText: suspend (String) -> Unit, onReasoning: suspend (String) -> Unit, ): List { val toolCalls = mutableListOf() val geminiTools = - if (toolResults == null && tools.isNotEmpty()) { + if (tools.isNotEmpty()) { listOf( GeminiTools( functionDeclarations = @@ -117,43 +117,44 @@ class GeminiClient( } val contents = - if (toolResults != null) { + if (maxAgentSteps != null) { historyContents + listOf( - GeminiContent( - role = "user", - parts = listOf(GeminiPart(text = userMessage)), - ), - GeminiContent( - role = "model", - parts = - toolResults.map { tr -> - GeminiPart( - functionCall = - GeminiFunctionCall( - name = tr.call.name, - args = - Json.parseToJsonElement(tr.call.arguments) - .jsonObject, - ) - ) - }, - ), - GeminiContent( - role = "user", - parts = - toolResults.map { tr -> - GeminiPart( - functionResponse = - GeminiFunctionResponse( - name = tr.call.name, - response = - buildJsonObject { put("result", tr.result) }, - ) - ) - }, - ), - ) + GeminiContent(role = "user", parts = listOf(GeminiPart(text = userMessage))) + ) + + maxAgentSteps.flatMap { roundtrip -> + listOf( + GeminiContent( + role = "model", + parts = + roundtrip.map { tr -> + GeminiPart( + functionCall = + GeminiFunctionCall( + name = tr.call.name, + args = + Json.parseToJsonElement(tr.call.arguments) + .jsonObject, + ) + ) + }, + ), + GeminiContent( + role = "user", + parts = + roundtrip.map { tr -> + GeminiPart( + functionResponse = + GeminiFunctionResponse( + name = tr.call.name, + response = + buildJsonObject { put("result", tr.result) }, + ) + ) + }, + ), + ) + } } else { historyContents + listOf( diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt index c5af73a80bb..413316e83f0 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt @@ -48,7 +48,7 @@ interface LlmClient { userMessage: String, conversationHistory: List = emptyList(), tools: List = emptyList(), - toolResults: List? = null, + maxAgentSteps: List>? = null, onText: suspend (String) -> Unit, onReasoning: suspend (String) -> Unit = {}, ): List diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt index 359cd2338e7..50ed4a6b939 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt @@ -91,13 +91,15 @@ class OpenAiClient( userMessage: String, conversationHistory: List, tools: List, - toolResults: List?, + maxAgentSteps: List>?, onText: suspend (String) -> Unit, onReasoning: suspend (String) -> Unit, ): List { val collectedToolCalls = mutableListOf() val accumulatedToolCalls = mutableListOf() + var callIdCounter = 0 + val messages = buildList { add(OpenAiMessage(role = "system", content = JsonPrimitive(SYSTEM_PROMPT))) @@ -109,39 +111,43 @@ class OpenAiClient( add(OpenAiMessage(role = "user", content = JsonPrimitive(userMessage))) - if (toolResults != null) { - add( - OpenAiMessage( - role = "assistant", - content = JsonPrimitive(""), - toolCalls = - toolResults.mapIndexed { index, tr -> - OpenAiToolCall( - id = "call_$index", - type = "function", - function = - OpenAiFunctionCall( - name = tr.call.name, - arguments = tr.call.arguments, - ), - ) - }, - ) - ) - toolResults.forEachIndexed { index, tr -> + if (maxAgentSteps != null) { + for (agentStep in maxAgentSteps) { + val startId = callIdCounter add( OpenAiMessage( - role = "tool", - toolCallId = "call_$index", - content = JsonPrimitive(tr.result), + role = "assistant", + content = JsonPrimitive(""), + toolCalls = + agentStep.mapIndexed { index, tr -> + OpenAiToolCall( + id = "call_${startId + index}", + type = "function", + function = + OpenAiFunctionCall( + name = tr.call.name, + arguments = tr.call.arguments, + ), + ) + }, ) ) + agentStep.forEachIndexed { index, tr -> + add( + OpenAiMessage( + role = "tool", + toolCallId = "call_${startId + index}", + content = JsonPrimitive(tr.result), + ) + ) + } + callIdCounter += agentStep.size } } } val openAiTools = - if (toolResults == null && tools.isNotEmpty()) { + if (tools.isNotEmpty()) { tools.map { tool -> OpenAiTool( type = "function", diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt index 6b705b11fa0..fe2c63114d9 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt @@ -33,7 +33,13 @@ import kotlinx.serialization.json.put const val SYSTEM_PROMPT = "You are a code analysis assistant with access to CPG (Code Property Graph) tools. " + - "Use multiple tools when needed to answer thoroughly. " + + "You have ONLY the tools listed in the tool definitions — do NOT invent or guess tool names. " + + "When the user asks a question about the code, use your tools when needed, never ask the user to do the analysis themselves. " + + "Follow a multi-step approach: " + + "1) First use listing tools to get an overview (e.g., cpg_list_functions for function summaries with names, parameters, and callees). " + + "2) Then use cpg_get_node with specific IDs to inspect the actual source code of functions that look relevant. " + + "Do NOT stop at summaries alone — always retrieve and inspect the code before drawing conclusions. " + + "If you can answer from previous tool results already in the conversation, respond directly without calling tools again. " + "Explain your findings clearly." suspend fun readSseStream(channel: ByteReadChannel, processLine: suspend (String) -> Unit) { diff --git a/codyze-console/src/main/resources/application.conf b/codyze-console/src/main/resources/application.conf index c4dea0b4c04..1dc4f096f0a 100644 --- a/codyze-console/src/main/resources/application.conf +++ b/codyze-console/src/main/resources/application.conf @@ -1,5 +1,5 @@ llm { - client = "ollama" # Options: "ollama", "gemini", "vLLM" + client = "gemini" # Options: "ollama", "gemini", "vLLM" gemini { baseUrl = "https://generativelanguage.googleapis.com/v1beta" diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte index 1d1679db75b..a19162aab8b 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte @@ -1,7 +1,7 @@ +
@@ -166,7 +163,7 @@ - {toolDisplayName} + {widgetTitle}
{items.length} result{items.length !== 1 ? 's' : ''}
@@ -177,7 +174,7 @@

No results

{:else} - {#each items as item (item.id)} + {#each visibleItems as item (item.id)}
{/if} @@ -229,6 +226,12 @@ {/if}
{/each} + + {#if hasMore && !showAll} + + {/if} {/if}

@@ -417,6 +420,27 @@ } .view-btn:hover { background: #f9fafb; border-color: #3b82f6; color: #3b82f6; } + .show-more-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 0.625rem 1rem; + background: white; + border: 1px dashed #d1d5db; + border-radius: 0.5rem; + font-size: 0.8125rem; + font-weight: 500; + color: #6b7280; + cursor: pointer; + transition: all 0.15s; + width: 100%; + } + .show-more-btn:hover { + background: #f9fafb; + border-color: #3b82f6; + color: #3b82f6; + } + .empty-state { display: flex; align-items: center; diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte deleted file mode 100644 index f536558cb5f..00000000000 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/ToolResultWidget.svelte +++ /dev/null @@ -1,132 +0,0 @@ - - -
- {#if selectedWidget} - - {@const SelectedWidget = selectedWidget} - - {:else} - -
-
- {#if data.toolName} - {data.toolName} - {/if} - {#if data.isError} - Error - {/if} -
-
{renderFallback(data.content)}
-
- {/if} -
- - diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts index 6a8e31328c1..0b484e96b75 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/widgetRegistry.ts @@ -1,46 +1,5 @@ -import type { Component } from 'svelte'; - export type ToolResultData = { toolName?: string; content: any; isError?: boolean; }; - -export type WidgetProps = { - data: ToolResultData; - onItemClick?: (item: any) => void; -}; - -export type WidgetComponent = Component; - -type WidgetMatcher = (data: ToolResultData) => boolean; - -interface WidgetRegistration { - component: WidgetComponent; - matcher: WidgetMatcher; -} - -class WidgetRegistry { - private registrations: WidgetRegistration[] = []; - - register(component: WidgetComponent, matcher: WidgetMatcher) { - this.registrations.push({ component, matcher }); - } - - /** - * Find the appropriate widget component for the given data. - * Returns null if no matching widget is found. - */ - getWidget(data: ToolResultData): WidgetComponent | null { - for (let i = 0; i < this.registrations.length; i++) { - const registration = this.registrations[i]; - const matches = registration.matcher(data); - if (matches) { - return registration.component; - } - } - return null; - } -} - -export const widgetRegistry = new WidgetRegistry(); \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte b/codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte index 9935b5368e8..1285297f17c 100644 --- a/codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/analysis/CodeViewer.svelte @@ -5,8 +5,42 @@ import { flattenNodes } from '$lib/flatten'; import Highlight, { LineNumbers } from 'svelte-highlight'; import python from 'svelte-highlight/languages/python'; + import java from 'svelte-highlight/languages/java'; + import cpp from 'svelte-highlight/languages/cpp'; + import csharp from 'svelte-highlight/languages/csharp'; + import javascript from 'svelte-highlight/languages/javascript'; + import typescript from 'svelte-highlight/languages/typescript'; + import go from 'svelte-highlight/languages/go'; + import rust from 'svelte-highlight/languages/rust'; + import ruby from 'svelte-highlight/languages/ruby'; + import plaintext from 'svelte-highlight/languages/plaintext'; import 'svelte-highlight/styles/github.css'; + const languageMap: Record = { + '.py': python, + '.java': java, + '.kt': java, // Kotlin uses Java highlighting as fallback + '.c': cpp, + '.cpp': cpp, + '.cc': cpp, + '.cxx': cpp, + '.h': cpp, + '.hpp': cpp, + '.cs': csharp, + '.js': javascript, + '.jsx': javascript, + '.ts': typescript, + '.tsx': typescript, + '.go': go, + '.rs': rust, + '.rb': ruby, + }; + + function getLanguage(fileName: string) { + const ext = fileName.substring(fileName.lastIndexOf('.')); + return languageMap[ext] || plaintext; + } + interface Props { translationUnit: TranslationUnitJSON; astNodes: NodeJSON[]; @@ -114,7 +148,7 @@
- + { @@ -58,11 +59,17 @@ } function resetChat() { + // Abort any in-flight stream + if (abortController) { + abortController.abort(); + abortController = null; + } showWelcome = true; chatMessages = []; currentMessage = ''; streamingContent = ''; streamingReasoning = ''; + isLoading = false; } async function sendMessage() { diff --git a/codyze-core/src/integrationTest/resources/example/src/module1/ghidra-export4851929095593266204.cpp b/codyze-core/src/integrationTest/resources/example/src/module1/ghidra-export4851929095593266204.cpp new file mode 100644 index 00000000000..5533dc91c61 --- /dev/null +++ b/codyze-core/src/integrationTest/resources/example/src/module1/ghidra-export4851929095593266204.cpp @@ -0,0 +1,651 @@ +#include "ghidra-export4851929095593266204.h" + + + +void _DT_INIT(void) + +{ + __gmon_start__(); + return; +} + + + +void FUN_00101020(void) + +{ + (*(code *)(undefined *)0x0)(); + return; +} + + + +// WARNING: Unknown calling convention -- yet parameter storage is locked + +size_t fread(void *__ptr,size_t __size,size_t __n,FILE *__stream) + +{ + size_t sVar1; + + sVar1 = fread(__ptr,__size,__n,__stream); + return sVar1; +} + + + +void __stack_chk_fail(void) + +{ + // WARNING: Subroutine does not return + __stack_chk_fail(); +} + + + +// WARNING: Unknown calling convention -- yet parameter storage is locked + +int feof(FILE *__stream) + +{ + int iVar1; + + iVar1 = feof(__stream); + return iVar1; +} + + + +// WARNING: Unknown calling convention -- yet parameter storage is locked + +size_t fwrite(void *__ptr,size_t __size,size_t __n,FILE *__s) + +{ + size_t sVar1; + + sVar1 = fwrite(__ptr,__size,__n,__s); + return sVar1; +} + + + +undefined8 FUN_00101070(void) + +{ + int iVar1; + long in_FS_OFFSET; + undefined1 auStack_f8 [192]; + undefined1 local_38 [24]; + long local_20; + + local_20 = *(long *)(in_FS_OFFSET + 0x28); + FUN_00101240(); + fread(auStack_f8,4,4,stdin); + while( true ) { + iVar1 = feof(stdin); + if (iVar1 != 0) break; + fread(local_38,4,4,stdin); + iVar1 = feof(stdin); + if (iVar1 == 0) { + FUN_00101740(auStack_f8); + fwrite(local_38,4,4,stdout); + } + } + if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) { + return 0; + } + // WARNING: Subroutine does not return + __stack_chk_fail(); +} + + + +void processEntry entry(undefined8 param_1,undefined8 param_2) + +{ + undefined1 auStack_8 [8]; + + __libc_start_main(FUN_00101070,param_2,&stack0x00000008,0,0,param_1,auStack_8); + do { + // WARNING: Do nothing block with infinite loop + } while( true ); +} + + + +// WARNING: Removing unreachable block (ram,0x00101183) +// WARNING: Removing unreachable block (ram,0x0010118f) + +void FUN_00101170(void) + +{ + return; +} + + + +// WARNING: Removing unreachable block (ram,0x001011c4) +// WARNING: Removing unreachable block (ram,0x001011d0) + +void FUN_001011a0(void) + +{ + return; +} + + + +void _FINI_0(void) + +{ + if (DAT_00104358 != '\0') { + return; + } + __cxa_finalize(PTR_LOOP_00104028); + FUN_00101170(); + DAT_00104358 = 1; + return; +} + + + +void _INIT_0(void) + +{ + FUN_001011a0(); + return; +} + + + +void FUN_00101240(void) + +{ + FUN_00101c50(&DAT_00104360); + return; +} + + + +void FUN_00101250(undefined8 *param_1) + +{ + uint uVar1; + uint *puVar2; + uint uVar3; + uint *puVar4; + int iVar5; + long lVar6; + + puVar4 = (uint *)(param_1 + 2); + uVar3 = 4; + param_1[2] = *param_1; + param_1[3] = param_1[1]; + iVar5 = 1; + do { + uVar1 = puVar4[3]; + puVar2 = puVar4; + while ((uVar3 & 3) != 0) { + uVar3 = uVar3 + 1; + puVar2[4] = *puVar2 ^ uVar1; + if (uVar3 == 0x2c) { + return; + } + uVar1 = puVar2[4]; + puVar2 = puVar2 + 1; + } + lVar6 = (long)iVar5; + uVar3 = uVar3 + 1; + puVar4 = puVar2 + 1; + iVar5 = iVar5 + 1; + puVar2[4] = CONCAT31(CONCAT21(CONCAT11((&DAT_00104360)[uVar1 & 0xff], + (&DAT_00104360)[uVar1 >> 0x18]), + (&DAT_00104360)[uVar1 >> 0x10 & 0xff]), + (&DAT_00104240)[lVar6] ^ (&DAT_00104360)[uVar1 >> 8 & 0xff]) ^ *puVar2; + } while( true ); +} + + + +void FUN_00101320(long param_1,int param_2) + +{ + ulong *puVar1; + ulong uVar2; + + puVar1 = (ulong *)(param_1 + 0x10 + (long)param_2 * 0x10); + uVar2 = puVar1[1]; + *(ulong *)(param_1 + 0xc0) = *(ulong *)(param_1 + 0xc0) ^ *puVar1; + *(ulong *)(param_1 + 200) = *(ulong *)(param_1 + 200) ^ uVar2; + return; +} + + + +void FUN_00101340(byte *param_1) + +{ + *param_1 = (&DAT_00104360)[*param_1]; + param_1[1] = (&DAT_00104360)[param_1[1]]; + param_1[2] = (&DAT_00104360)[param_1[2]]; + param_1[3] = (&DAT_00104360)[param_1[3]]; + param_1[4] = (&DAT_00104360)[param_1[4]]; + param_1[5] = (&DAT_00104360)[param_1[5]]; + param_1[6] = (&DAT_00104360)[param_1[6]]; + param_1[7] = (&DAT_00104360)[param_1[7]]; + param_1[8] = (&DAT_00104360)[param_1[8]]; + param_1[9] = (&DAT_00104360)[param_1[9]]; + param_1[10] = (&DAT_00104360)[param_1[10]]; + param_1[0xb] = (&DAT_00104360)[param_1[0xb]]; + param_1[0xc] = (&DAT_00104360)[param_1[0xc]]; + param_1[0xd] = (&DAT_00104360)[param_1[0xd]]; + param_1[0xe] = (&DAT_00104360)[param_1[0xe]]; + param_1[0xf] = (&DAT_00104360)[param_1[0xf]]; + return; +} + + + +void FUN_00101400(long param_1) + +{ + undefined1 uVar1; + undefined1 uVar2; + + uVar1 = *(undefined1 *)(param_1 + 5); + uVar2 = *(undefined1 *)(param_1 + 0xd); + *(ushort *)(param_1 + 5) = CONCAT11(*(undefined1 *)(param_1 + 0xe),*(undefined1 *)(param_1 + 9)); + *(ushort *)(param_1 + 0xd) = CONCAT11(*(undefined1 *)(param_1 + 6),*(undefined1 *)(param_1 + 1)); + *(ushort *)(param_1 + 1) = CONCAT11(*(undefined1 *)(param_1 + 10),uVar1); + *(ushort *)(param_1 + 9) = CONCAT11(*(undefined1 *)(param_1 + 2),uVar2); + uVar1 = *(undefined1 *)(param_1 + 0xb); + *(undefined1 *)(param_1 + 0xb) = *(undefined1 *)(param_1 + 7); + *(undefined1 *)(param_1 + 7) = *(undefined1 *)(param_1 + 3); + uVar2 = *(undefined1 *)(param_1 + 0xf); + *(undefined1 *)(param_1 + 0xf) = uVar1; + *(undefined1 *)(param_1 + 3) = uVar2; + return; +} + + + +void FUN_00101450(byte *param_1) + +{ + byte bVar1; + byte bVar2; + byte bVar3; + byte bVar4; + byte bVar5; + byte bVar6; + byte bVar7; + byte bVar8; + byte bVar9; + byte bVar10; + byte bVar11; + byte bVar12; + byte bVar13; + byte bVar14; + byte bVar15; + byte bVar16; + byte bVar17; + byte bVar18; + byte bVar19; + byte bVar20; + byte bVar21; + byte bVar22; + byte bVar23; + byte bVar24; + byte bVar25; + byte bVar26; + byte bVar27; + byte bVar28; + byte bVar29; + byte bVar30; + byte bVar31; + + bVar9 = *param_1; + bVar10 = param_1[1]; + bVar11 = param_1[3]; + bVar12 = param_1[2]; + bVar13 = param_1[4]; + bVar14 = param_1[5]; + bVar15 = param_1[6]; + bVar16 = param_1[7]; + bVar17 = param_1[9]; + bVar18 = param_1[8]; + bVar19 = param_1[0xb]; + bVar20 = (&DAT_00104140)[bVar18]; + bVar1 = (&DAT_00104040)[bVar17]; + bVar21 = (&DAT_00104140)[bVar17]; + bVar2 = (&DAT_00104040)[param_1[10]]; + bVar22 = (&DAT_00104140)[param_1[10]]; + bVar3 = (&DAT_00104040)[bVar19]; + bVar23 = param_1[0xe]; + bVar24 = param_1[0xc]; + bVar25 = (&DAT_00104040)[bVar18]; + bVar4 = (&DAT_00104140)[bVar19]; + bVar26 = param_1[0xd]; + bVar27 = param_1[0xf]; + bVar28 = (&DAT_00104140)[bVar24]; + bVar5 = (&DAT_00104040)[bVar26]; + bVar29 = (&DAT_00104140)[bVar26]; + bVar6 = (&DAT_00104040)[bVar23]; + bVar30 = (&DAT_00104140)[bVar23]; + bVar7 = (&DAT_00104040)[bVar27]; + bVar31 = (&DAT_00104040)[bVar24]; + bVar8 = (&DAT_00104140)[bVar27]; + *(ulong *)param_1 = + CONCAT17(bVar15 ^ bVar14 ^ (&DAT_00104140)[bVar16] ^ (&DAT_00104040)[bVar13], + CONCAT16(bVar13 ^ bVar14 ^ (&DAT_00104040)[bVar16] ^ (&DAT_00104140)[bVar15], + CONCAT15((&DAT_00104040)[bVar15] ^ (&DAT_00104140)[bVar14] ^ + bVar16 ^ bVar13, + CONCAT14((&DAT_00104040)[bVar14] ^ (&DAT_00104140)[bVar13] ^ + bVar16 ^ bVar15, + CONCAT13((&DAT_00104140)[bVar11] ^ (&DAT_00104040)[bVar9] + ^ bVar12 ^ bVar10, + CONCAT12((&DAT_00104040)[bVar11] ^ + (&DAT_00104140)[bVar12] ^ + bVar9 ^ bVar10, + CONCAT11((&DAT_00104040)[bVar12] ^ + (&DAT_00104140)[bVar10] ^ + bVar11 ^ bVar9, + (&DAT_00104040)[bVar10] ^ + (&DAT_00104140)[bVar9] ^ + bVar11 ^ bVar12))))))); + *(ulong *)(param_1 + 8) = + CONCAT17(bVar26 ^ bVar23 ^ bVar31 ^ bVar8, + CONCAT16(bVar24 ^ bVar26 ^ bVar30 ^ bVar7, + CONCAT15(bVar29 ^ bVar6 ^ bVar24 ^ bVar27, + CONCAT14(bVar28 ^ bVar5 ^ bVar23 ^ bVar27, + CONCAT13(bVar17 ^ param_1[10] ^ bVar25 ^ bVar4, + CONCAT12(bVar22 ^ bVar3 ^ bVar18 ^ bVar17, + CONCAT11(bVar18 ^ bVar19 ^ + bVar21 ^ bVar2, + bVar20 ^ bVar1 ^ + param_1[10] ^ bVar19))))))); + return; +} + + + +void FUN_00101740(long param_1) + +{ + undefined1 uVar1; + undefined1 uVar2; + undefined1 uVar3; + undefined1 uVar4; + undefined1 uVar5; + undefined1 uVar6; + undefined1 uVar7; + undefined1 uVar8; + undefined1 uVar9; + undefined1 uVar10; + undefined1 uVar11; + undefined1 uVar12; + undefined1 auVar13 [16]; + ulong uVar14; + ulong uVar15; + undefined1 (*pauVar16) [16]; + undefined1 (*pauVar17) [16]; + ulong uVar18; + ulong uVar19; + ulong uVar20; + uint uVar21; + ulong uVar22; + ulong uVar23; + ulong uVar24; + uint uVar25; + ulong uVar26; + uint uVar27; + ulong uVar28; + ulong uVar29; + byte local_4d; + byte local_4c; + byte local_4b; + byte local_4a; + byte local_49; + + FUN_00101250(); + auVar13 = *(undefined1 (*) [16])(param_1 + 0xc0) ^ *(undefined1 (*) [16])(param_1 + 0x10); + vpextrb_avx(auVar13,0xc); + vpextrb_avx(auVar13,0xd); + *(undefined1 (*) [16])(param_1 + 0xc0) = auVar13; + uVar25 = vpextrb_avx(auVar13,0); + uVar26 = (ulong)uVar25; + vpextrb_avx(auVar13,0xe); + vpextrb_avx(auVar13,0xf); + vpextrb_avx(auVar13,9); + uVar25 = vpextrb_avx(auVar13,1); + uVar24 = (ulong)uVar25; + uVar25 = vpextrb_avx(auVar13,2); + uVar23 = (ulong)uVar25; + uVar25 = vpextrb_avx(auVar13,3); + uVar22 = (ulong)uVar25; + uVar25 = vpextrb_avx(auVar13,4); + uVar20 = (ulong)uVar25; + uVar25 = vpextrb_avx(auVar13,5); + uVar19 = (ulong)uVar25; + uVar25 = vpextrb_avx(auVar13,6); + uVar18 = (ulong)uVar25; + uVar25 = vpextrb_avx(auVar13,7); + uVar14 = (ulong)uVar25; + uVar25 = vpextrb_avx(auVar13,8); + uVar15 = (ulong)uVar25; + uVar25 = vpextrb_avx(auVar13,10); + uVar29 = (ulong)uVar25; + uVar25 = vpextrb_avx(auVar13,0xb); + uVar28 = (ulong)uVar25; + pauVar16 = (undefined1 (*) [16])(param_1 + 0x20); + do { + pauVar17 = pauVar16 + 1; + *(undefined *)(param_1 + 0xc0) = (&DAT_00104360)[uVar26 & 0xff]; + uVar1 = (&DAT_00104360)[uVar24 & 0xff]; + *(undefined1 *)(param_1 + 0xc1) = uVar1; + uVar2 = (&DAT_00104360)[uVar23 & 0xff]; + *(undefined1 *)(param_1 + 0xc2) = uVar2; + uVar3 = (&DAT_00104360)[uVar22 & 0xff]; + *(undefined1 *)(param_1 + 0xc3) = uVar3; + *(undefined *)(param_1 + 0xc4) = (&DAT_00104360)[uVar20 & 0xff]; + uVar4 = (&DAT_00104360)[uVar19 & 0xff]; + *(undefined1 *)(param_1 + 0xc5) = uVar4; + uVar5 = (&DAT_00104360)[uVar18 & 0xff]; + *(undefined1 *)(param_1 + 0xc6) = uVar5; + uVar6 = (&DAT_00104360)[uVar14 & 0xff]; + *(undefined1 *)(param_1 + 199) = uVar6; + *(undefined *)(param_1 + 200) = (&DAT_00104360)[uVar15 & 0xff]; + uVar7 = (&DAT_00104360)[local_49]; + *(undefined1 *)(param_1 + 0xc9) = uVar7; + uVar8 = (&DAT_00104360)[uVar29 & 0xff]; + *(undefined1 *)(param_1 + 0xca) = uVar8; + uVar9 = (&DAT_00104360)[uVar28 & 0xff]; + *(undefined1 *)(param_1 + 0xcb) = uVar9; + *(undefined *)(param_1 + 0xcc) = (&DAT_00104360)[local_4d]; + uVar10 = (&DAT_00104360)[local_4c]; + *(undefined1 *)(param_1 + 0xcd) = uVar10; + uVar11 = (&DAT_00104360)[local_4b]; + *(undefined1 *)(param_1 + 0xce) = uVar11; + uVar12 = (&DAT_00104360)[local_4a]; + *(undefined1 *)(param_1 + 0xc1) = uVar4; + *(undefined1 *)(param_1 + 0xc5) = uVar7; + *(undefined1 *)(param_1 + 0xc9) = uVar10; + *(undefined1 *)(param_1 + 0xcd) = uVar1; + *(undefined1 *)(param_1 + 0xc6) = uVar11; + *(undefined1 *)(param_1 + 0xce) = uVar5; + *(undefined1 *)(param_1 + 0xc2) = uVar8; + *(undefined1 *)(param_1 + 0xca) = uVar2; + *(undefined1 *)(param_1 + 0xcb) = uVar6; + *(undefined1 *)(param_1 + 199) = uVar3; + *(undefined1 *)(param_1 + 0xc3) = uVar12; + *(undefined1 *)(param_1 + 0xcf) = uVar9; + FUN_00101450(param_1 + 0xc0); + local_49 = *(byte *)(param_1 + 0xc9) ^ (*pauVar16)[9]; + auVar13 = *pauVar16 ^ *(undefined1 (*) [16])(param_1 + 0xc0); + local_4d = *(byte *)(param_1 + 0xcc) ^ (*pauVar16)[0xc]; + uVar26 = (ulong)(*(byte *)(param_1 + 0xc0) ^ (*pauVar16)[0]); + uVar24 = (ulong)(*(byte *)(param_1 + 0xc1) ^ (*pauVar16)[1]); + local_4c = *(byte *)(param_1 + 0xcd) ^ (*pauVar16)[0xd]; + uVar23 = (ulong)(*(byte *)(param_1 + 0xc2) ^ (*pauVar16)[2]); + uVar22 = (ulong)(*(byte *)(param_1 + 0xc3) ^ (*pauVar16)[3]); + uVar20 = (ulong)(*(byte *)(param_1 + 0xc4) ^ (*pauVar16)[4]); + uVar19 = (ulong)(*(byte *)(param_1 + 0xc5) ^ (*pauVar16)[5]); + uVar18 = (ulong)(*(byte *)(param_1 + 0xc6) ^ (*pauVar16)[6]); + uVar14 = (ulong)(*(byte *)(param_1 + 199) ^ (*pauVar16)[7]); + uVar15 = (ulong)(*(byte *)(param_1 + 200) ^ (*pauVar16)[8]); + uVar29 = (ulong)(*(byte *)(param_1 + 0xca) ^ (*pauVar16)[10]); + uVar28 = (ulong)(*(byte *)(param_1 + 0xcb) ^ (*pauVar16)[0xb]); + local_4b = *(byte *)(param_1 + 0xce) ^ (*pauVar16)[0xe]; + local_4a = *(byte *)(param_1 + 0xcf) ^ (*pauVar16)[0xf]; + *(undefined1 (*) [16])(param_1 + 0xc0) = auVar13; + pauVar16 = pauVar17; + } while ((undefined1 (*) [16])(param_1 + 0xb0) != pauVar17); + uVar25 = vpextrb_avx(auVar13,0); + uVar21 = vpextrb_avx(auVar13,0xc); + uVar27 = vpextrb_avx(auVar13,0xf); + *(undefined *)(param_1 + 0xc0) = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,1); + uVar1 = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,2); + *(undefined1 *)(param_1 + 0xc1) = uVar1; + uVar2 = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,3); + *(undefined1 *)(param_1 + 0xc2) = uVar2; + uVar3 = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,4); + *(undefined1 *)(param_1 + 0xc3) = uVar3; + *(undefined *)(param_1 + 0xc4) = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,5); + uVar4 = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,6); + *(undefined1 *)(param_1 + 0xc5) = uVar4; + uVar5 = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,7); + *(undefined1 *)(param_1 + 0xc6) = uVar5; + uVar6 = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,8); + *(undefined1 *)(param_1 + 199) = uVar6; + *(undefined *)(param_1 + 200) = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,9); + uVar7 = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,10); + *(undefined1 *)(param_1 + 0xc9) = uVar7; + uVar8 = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,0xb); + *(undefined1 *)(param_1 + 0xca) = uVar8; + uVar9 = (&DAT_00104360)[uVar25]; + *(undefined1 *)(param_1 + 0xcb) = uVar9; + *(undefined *)(param_1 + 0xcc) = (&DAT_00104360)[uVar21]; + uVar25 = vpextrb_avx(auVar13,0xd); + uVar10 = (&DAT_00104360)[uVar25]; + uVar25 = vpextrb_avx(auVar13,0xe); + *(undefined1 *)(param_1 + 0xcd) = uVar10; + uVar11 = (&DAT_00104360)[uVar25]; + *(undefined1 *)(param_1 + 0xce) = uVar11; + uVar12 = (&DAT_00104360)[uVar27]; + *(undefined1 *)(param_1 + 0xc1) = uVar4; + *(undefined1 *)(param_1 + 0xc5) = uVar7; + *(undefined1 *)(param_1 + 0xc9) = uVar10; + *(undefined1 *)(param_1 + 0xcd) = uVar1; + *(undefined1 *)(param_1 + 0xc6) = uVar11; + *(undefined1 *)(param_1 + 0xce) = uVar5; + *(undefined1 *)(param_1 + 0xc2) = uVar8; + *(undefined1 *)(param_1 + 0xca) = uVar2; + *(undefined1 *)(param_1 + 0xcb) = uVar6; + *(undefined1 *)(param_1 + 199) = uVar3; + *(undefined1 *)(param_1 + 0xcf) = uVar9; + *(undefined1 *)(param_1 + 0xc3) = uVar12; + *(ulong *)(param_1 + 0xc0) = *(ulong *)(param_1 + 0xc0) ^ *(ulong *)(param_1 + 0xb0); + *(ulong *)(param_1 + 200) = *(ulong *)(param_1 + 200) ^ *(ulong *)(param_1 + 0xb8); + return; +} + + + +// WARNING: Globals starting with '_' overlap smaller symbols at the same address + +void FUN_00101bd0(uint *param_1,int param_2,undefined8 param_3,undefined8 param_4) + +{ + byte bVar1; + undefined4 uVar2; + undefined8 in_RAX; + uint uVar3; + undefined1 auVar4 [16]; + + uVar3 = *param_1; + auVar4 = vpshufb_avx(ZEXT416(uVar3),_DAT_00102010); + uVar2 = vpextrb_avx(ZEXT416(uVar3),1); + *param_1 = auVar4._0_4_; + bVar1 = (&DAT_00104360)[CONCAT44((int)((ulong)in_RAX >> 0x20),uVar2)]; + auVar4 = ZEXT416(uVar3); + uVar2 = vpextrb_avx(auVar4,2); + *(byte *)param_1 = bVar1; + *(undefined *)((long)param_1 + 1) = (&DAT_00104360)[CONCAT44((int)((ulong)param_4 >> 0x20),uVar2)] + ; + uVar3 = vpextrb_avx(auVar4,3); + *(undefined *)((long)param_1 + 2) = (&DAT_00104360)[uVar3]; + uVar3 = vpextrb_avx(auVar4,0); + *(undefined *)((long)param_1 + 3) = (&DAT_00104360)[uVar3]; + *(byte *)param_1 = bVar1 ^ (&DAT_00104240)[param_2]; + return; +} + + + +// WARNING: Globals starting with '_' overlap smaller symbols at the same address + +void FUN_00101c30(uint *param_1) + +{ + undefined1 auVar1 [16]; + + auVar1 = vpshufb_avx(ZEXT416(*param_1),_DAT_00102010); + *param_1 = auVar1._0_4_; + return; +} + + + +void FUN_00101c50(undefined1 *param_1) + +{ + byte bVar1; + uint uVar2; + uint uVar3; + byte bVar4; + bool bVar5; + + uVar2 = 1; + bVar1 = 1; + do { + bVar4 = bVar1 * '\x02' ^ bVar1; + bVar5 = (char)bVar1 < '\0'; + bVar1 = bVar4; + if (bVar5) { + bVar1 = bVar4 ^ 0x1b; + } + uVar2 = uVar2 * 2 ^ uVar2; + uVar2 = uVar2 * 4 ^ uVar2; + uVar3 = uVar2 << 4 ^ uVar2; + uVar2 = uVar3 ^ 9; + if (-1 < (char)uVar3) { + uVar2 = uVar3; + } + bVar4 = (byte)uVar2; + param_1[bVar1] = + (bVar4 << 4 | bVar4 >> 4) ^ (bVar4 << 1 | (char)bVar4 < '\0') ^ bVar4 ^ + (bVar4 << 3 | bVar4 >> 5) ^ (bVar4 << 2 | bVar4 >> 6) ^ 99; + } while (bVar1 != 1); + *param_1 = 99; + return; +} + + + +void _DT_FINI(void) + +{ + return; +} + + diff --git a/codyze-core/src/integrationTest/resources/example/src/module1/main.py b/codyze-core/src/integrationTest/resources/example/src/module1/main.py deleted file mode 100644 index 94284738456..00000000000 --- a/codyze-core/src/integrationTest/resources/example/src/module1/main.py +++ /dev/null @@ -1,6 +0,0 @@ -from library.lib import special_func - -key = get_secret_from_server() - -err = encrypt("Hello World", key, cipher = "AES-256") -special_func() diff --git a/codyze-core/src/integrationTest/resources/example/src/module2/pkg/mod.py b/codyze-core/src/integrationTest/resources/example/src/module2/pkg/mod.py deleted file mode 100644 index 9332a2735b6..00000000000 --- a/codyze-core/src/integrationTest/resources/example/src/module2/pkg/mod.py +++ /dev/null @@ -1,2 +0,0 @@ -def foo(): - pass diff --git a/codyze-core/src/integrationTest/resources/example/src/third-party/mylib/library/lib.py b/codyze-core/src/integrationTest/resources/example/src/third-party/mylib/library/lib.py deleted file mode 100644 index 4680aa75ff6..00000000000 --- a/codyze-core/src/integrationTest/resources/example/src/third-party/mylib/library/lib.py +++ /dev/null @@ -1,2 +0,0 @@ -def special_func(): - pass diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt index 7e1dac4b723..5e68d984504 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt @@ -33,10 +33,10 @@ import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions fun configureServer( configure: Server.() -> Server = { - this.addCpgTranslate() - this.addListPasses() - this.addRunPass() - this.addCpgAnalyzeTool() + // this.addCpgTranslate() + // this.addListPasses() + // this.addRunPass() + // this.addCpgAnalyzeTool() this.addCpgLlmAnalyzeTool() this.addCpgApplyConceptsTool() this.addCpgDataflowTool() @@ -44,11 +44,12 @@ fun configureServer( this.listRecords() this.listCalls() this.listCallsTo() - this.listAvailableConcepts() - this.listAvailableOperations() - this.getAllArgs() - this.getArgByIndexOrName() - this.listConceptsAndOperations() + // this.listAvailableConcepts() + // this.listAvailableOperations() + // this.getAllArgs() + // this.getArgByIndexOrName() + // this.listConceptsAndOperations() + this.getNode() this } ): Server { diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgAnalyzeTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgAnalyzeTool.kt index 623d03b348d..c55f20e31ea 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgAnalyzeTool.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgAnalyzeTool.kt @@ -64,12 +64,7 @@ import de.fraunhofer.aisec.cpg.graph.functions import de.fraunhofer.aisec.cpg.graph.nodes import de.fraunhofer.aisec.cpg.graph.variables import de.fraunhofer.aisec.cpg.mcp.mcpserver.cpgDescription -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalysisResult -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgAnalyzePayload -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgRunPassPayload -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.PassInfo -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.runOnCpg -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toObject +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.* import de.fraunhofer.aisec.cpg.mcp.setupTranslationConfiguration import de.fraunhofer.aisec.cpg.passes.BasicBlockCollectorPass import de.fraunhofer.aisec.cpg.passes.ComponentPass @@ -97,7 +92,6 @@ import de.fraunhofer.aisec.cpg.passes.configuration.ReplacePass import de.fraunhofer.aisec.cpg.passes.consumeTargets import de.fraunhofer.aisec.cpg.passes.hardDependencies import de.fraunhofer.aisec.cpg.passes.softDependencies -import de.fraunhofer.aisec.cpg.serialization.toJSON import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult @@ -236,14 +230,12 @@ fun runCpgAnalyze( val variables = result.variables val callExpressions = result.calls - val nodeInfos = allNodes.map { node: Node -> node.toJSON(noEdges = true) } - return CpgAnalysisResult( totalNodes = allNodes.size, functions = functions.size, variables = variables.size, callExpressions = callExpressions.size, - nodes = nodeInfos, + functionSummaries = functions.map { it.toSummary() }, ) } diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt index 6cb20cc80a1..010c87ef235 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt @@ -27,11 +27,7 @@ package de.fraunhofer.aisec.cpg.mcp.mcpserver.tools import de.fraunhofer.aisec.cpg.graph.nodes import de.fraunhofer.aisec.cpg.mcp.mcpserver.cpgDescription -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgLlmAnalyzePayload -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.OverlaySuggestion -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.getAvailableConcepts -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.getAvailableOperations -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toObject +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.* import de.fraunhofer.aisec.cpg.serialization.toJSON import io.modelcontextprotocol.kotlin.sdk.server.Server import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult @@ -243,14 +239,20 @@ fun Server.addCpgLlmAnalyzeTool() { val enrichedItems = parsed.items.map { suggestion -> val node = nodes.find { it.id.toString() == suggestion.nodeId } - val conceptNode = - suggestion.conceptNodeId?.let { id -> - nodes.find { it.id.toString() == id } - } suggestion.copy( - node = node?.toJSON(noEdges = true), - conceptNode = conceptNode?.toJSON(noEdges = true), + name = node?.name?.toString() ?: suggestion.name, + type = node?.javaClass?.simpleName ?: suggestion.type, + code = node?.code ?: suggestion.code, + fileName = + node?.location?.artifactLocation?.uri?.let { uri -> + uri.toString() + .substringAfterLast('/') + .substringAfterLast('\\') + } ?: suggestion.fileName, + startLine = + node?.location?.region?.startLine ?: suggestion.startLine, + endLine = node?.location?.region?.endLine ?: suggestion.endLine, ) } diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt index 17fb1a843ee..76396b37ca9 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt @@ -36,6 +36,7 @@ import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject @@ -53,7 +54,9 @@ fun Server.listFunctions() { this.addTool(name = "cpg_list_functions", description = toolDescription) { request -> request.runOnCpg { result: TranslationResult, _ -> - CallToolResult(content = result.functions.map { TextContent(it.toJson()) }) + CallToolResult( + content = result.functions.map { TextContent(Json.encodeToString(it.toSummary())) } + ) } } } @@ -61,7 +64,8 @@ fun Server.listFunctions() { fun Server.listRecords() { val toolDescription = """ - This tool lists all classes and structs, more precisely their declarations, which are held in the graph. + This tool lists all class and struct declarations as compact summaries. + Use cpg_get_node with a id to retrieve the full node details. Example prompts: - "Show me all classes in the code" @@ -71,7 +75,9 @@ fun Server.listRecords() { this.addTool(name = "cpg_list_records", description = toolDescription) { request -> request.runOnCpg { result: TranslationResult, _ -> - CallToolResult(content = result.records.map { TextContent(it.toJson()) }) + CallToolResult( + content = result.records.map { TextContent(Json.encodeToString(it.toSummary())) } + ) } } } @@ -95,8 +101,9 @@ fun Server.listConceptsAndOperations() { fun Server.listCalls() { val toolDescription = """ - This tool lists all function and method calls, which are held in the graph. - + This tool lists all function and method calls as compact summaries. + Use cpg_get_node with a id to retrieve the full node details. + Example prompts: - "Show me all function calls in the code" - "What functions are being called?" @@ -105,7 +112,9 @@ fun Server.listCalls() { this.addTool(name = "cpg_list_calls", description = toolDescription) { request -> request.runOnCpg { result: TranslationResult, _ -> - CallToolResult(content = result.calls.map { TextContent(it.toJson()) }) + CallToolResult( + content = result.calls.map { TextContent(Json.encodeToString(it.toSummary())) } + ) } } } @@ -289,3 +298,43 @@ fun Server.getArgByIndexOrName() { } } } + +fun Server.getNode() { + val toolDescription = + """ + Retrieves the complete information of a single node by its id, including its source code. + Use this after list commands to inspect the actual code and details of specific nodes. + """ + .trimIndent() + + val inputSchema = + ToolSchema( + properties = + buildJsonObject { + putJsonObject("id") { + put("type", "string") + put("description", "The id of the node to retrieve.") + } + }, + required = listOf("id"), + ) + + this.addTool(name = "cpg_get_node", description = toolDescription, inputSchema = inputSchema) { + request -> + request.runOnCpg { result: TranslationResult, request: CallToolRequest -> + val payload = + request.arguments?.toObject() + ?: return@runOnCpg CallToolResult( + content = + listOf(TextContent("Invalid or missing payload for cpg_get_node tool.")) + ) + + val node = result.nodes.find { it.id.toString() == payload.id } + if (node != null) { + CallToolResult(content = listOf(TextContent(node.toJson()))) + } else { + CallToolResult(content = listOf(TextContent("No node found with ${payload.id}"))) + } + } + } +} diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt index f5019aa6b69..778924d3ea4 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt @@ -25,7 +25,6 @@ */ package de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils -import de.fraunhofer.aisec.cpg.serialization.NodeJSON import kotlinx.serialization.Serializable @Serializable @@ -59,8 +58,12 @@ data class OverlaySuggestion( val arguments: Map? = null, val reasoning: String? = null, val securityImpact: String? = null, - val node: NodeJSON? = null, - val conceptNode: NodeJSON? = null, + val name: String? = null, + val type: String? = null, + val code: String? = null, + val fileName: String? = null, + val startLine: Int? = null, + val endLine: Int? = null, ) @Serializable data class CpgApplyConceptsPayload(val items: List) diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt index 927faeb3f15..cced313f596 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt @@ -58,13 +58,49 @@ data class OverlayInfo( ) } +@Serializable +data class FunctionSummary( + val id: String, + val name: String, + val fileName: String?, + val startLine: Int?, + val endLine: Int?, + val parameters: List, + val returnType: String?, + val callees: List, + val code: String?, +) + +@Serializable +data class RecordSummary( + val id: String, + val name: String, + val fileName: String?, + val startLine: Int?, + val endLine: Int?, + val kind: String?, + val fieldCount: Int, + val methodNames: List, +) + +@Serializable +data class CallSummary( + val id: String, + val name: String, + val fileName: String?, + val startLine: Int?, + val endLine: Int?, + val arguments: List, + val code: String?, +) + @Serializable data class CpgAnalysisResult( val totalNodes: Int, val functions: Int, val variables: Int, val callExpressions: Int, - val nodes: List, + val functionSummaries: List, ) @Serializable diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt index 7d0f71017e8..1c412671640 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt @@ -29,9 +29,13 @@ import de.fraunhofer.aisec.cpg.TranslationResult import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.Node import de.fraunhofer.aisec.cpg.graph.OverlayNode +import de.fraunhofer.aisec.cpg.graph.callees import de.fraunhofer.aisec.cpg.graph.concepts.Concept import de.fraunhofer.aisec.cpg.graph.concepts.Operation +import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration import de.fraunhofer.aisec.cpg.graph.listOverlayClasses +import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.globalAnalysisResult import de.fraunhofer.aisec.cpg.query.QueryTree import de.fraunhofer.aisec.cpg.serialization.NodeJSON @@ -57,6 +61,42 @@ fun Node.toJson() = Json.encodeToString(this.toJSON()) fun OverlayNode.toJson() = Json.encodeToString(OverlayInfo(this)) +fun FunctionDeclaration.toSummary() = + FunctionSummary( + id = this.id.toString(), + name = this.name.localName, + fileName = this.location?.artifactLocation?.fileName, + startLine = this.location?.region?.startLine, + endLine = this.location?.region?.endLine, + parameters = this.parameters.map { "${it.type.typeName} ${it.name.localName}" }, + returnType = this.returnTypes.firstOrNull()?.typeName, + callees = this.callees.map { it.name.localName }, + code = this.code, + ) + +fun RecordDeclaration.toSummary() = + RecordSummary( + id = this.id.toString(), + name = this.name.localName, + fileName = this.location?.artifactLocation?.fileName, + startLine = this.location?.region?.startLine, + endLine = this.location?.region?.endLine, + kind = this.kind, + fieldCount = this.fields.size, + methodNames = this.methods.map { it.name.localName }, + ) + +fun CallExpression.toSummary() = + CallSummary( + id = this.id.toString(), + name = this.name.localName, + fileName = this.location?.artifactLocation?.fileName, + startLine = this.location?.region?.startLine, + endLine = this.location?.region?.endLine, + arguments = this.arguments.map { "${it.type.typeName} ${it.name.localName}" }, + code = this.code, + ) + /** Returns all available concrete (non-abstract) concept classes. */ fun getAvailableConcepts(): List> { return listOverlayClasses() diff --git a/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt b/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt index 5160f9588b0..b8ff157e4a1 100644 --- a/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt +++ b/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt @@ -62,6 +62,7 @@ class CpgAnalyzeToolTest { "cpg_list_call_arg_by_name_or_index", "cpg_list_available_concepts", "cpg_list_available_operations", + "cpg_get_node", ), testServer.tools.keys, ) diff --git a/cpg-serialization/src/main/kotlin/de/fraunhofer/aisec/cpg/serialization/NodeSerialization.kt b/cpg-serialization/src/main/kotlin/de/fraunhofer/aisec/cpg/serialization/NodeSerialization.kt index c9bb323d83e..18c8792052f 100644 --- a/cpg-serialization/src/main/kotlin/de/fraunhofer/aisec/cpg/serialization/NodeSerialization.kt +++ b/cpg-serialization/src/main/kotlin/de/fraunhofer/aisec/cpg/serialization/NodeSerialization.kt @@ -25,7 +25,6 @@ */ package de.fraunhofer.aisec.cpg.serialization -import de.fraunhofer.aisec.cpg.graph.AstNode import de.fraunhofer.aisec.cpg.graph.Node import de.fraunhofer.aisec.cpg.graph.component import de.fraunhofer.aisec.cpg.graph.edges.Edge @@ -71,9 +70,9 @@ data class NodeJSON( val endColumn: Int, val code: String, val name: String, - val astChildren: List, - val prevDFG: List = emptyList(), - val nextDFG: List = emptyList(), + // val astChildren: List, + // val prevDFG: List = emptyList(), + // val nextDFG: List = emptyList(), @Serializable(with = UuidSerializer::class) val translationUnitId: Uuid? = null, val componentName: String? = null, val fileName: String? = null, @@ -96,11 +95,11 @@ fun Node.toJSON(noEdges: Boolean = false): NodeJSON { val path = uri.toString() path.substringAfterLast('/').substringAfterLast('\\') }, - astChildren = - if (noEdges) emptyList() - else (this as? AstNode)?.astChildren?.map { it.toJSON() } ?: emptyList(), - prevDFG = if (noEdges) emptyList() else this.prevDFGEdges.map { it.toJSON() }, - nextDFG = if (noEdges) emptyList() else this.nextDFGEdges.map { it.toJSON() }, + // astChildren = + // if (noEdges) emptyList() + // else (this as? AstNode)?.astChildren?.map { it.toJSON() } ?: emptyList(), + // prevDFG = if (noEdges) emptyList() else this.prevDFGEdges.map { it.toJSON() }, + // nextDFG = if (noEdges) emptyList() else this.nextDFGEdges.map { it.toJSON() }, translationUnitId = this.translationUnit?.id, componentName = this.component?.name?.toString(), ) From 7e0db4a67ac339f813d6dee221a5adbd4e15411a Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Wed, 18 Feb 2026 01:11:32 +0100 Subject: [PATCH 55/93] New tool and refactoring --- .../aisec/codyze/console/ai/clients/Util.kt | 5 +- .../components/ai-agent/WelcomeScreen.svelte | 8 +- .../webapp/src/routes/ai-agent/+page.svelte | 4 +- .../aisec/cpg/mcp/mcpserver/McpServer.kt | 7 +- .../mcp/mcpserver/tools/CpgDfgBackwardTool.kt | 116 ++++++++++++++++++ .../mcp/mcpserver/tools/utils/Serializable.kt | 6 +- .../cpg/mcp/mcpserver/tools/utils/Util.kt | 11 +- .../aisec/cpg/mcp/CpgAnalyzeToolTest.kt | 1 + .../cpg/serialization/NodeSerialization.kt | 17 +-- 9 files changed, 154 insertions(+), 21 deletions(-) create mode 100644 cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDfgBackwardTool.kt diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt index fe2c63114d9..d9b67d59088 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt @@ -34,12 +34,15 @@ import kotlinx.serialization.json.put const val SYSTEM_PROMPT = "You are a code analysis assistant with access to CPG (Code Property Graph) tools. " + "You have ONLY the tools listed in the tool definitions — do NOT invent or guess tool names. " + - "When the user asks a question about the code, use your tools when needed, never ask the user to do the analysis themselves. " + + "When the user asks a question about the code, use your tools immediately — never ask for permission or confirmation, never ask the user to do the analysis themselves. Act directly. " + "Follow a multi-step approach: " + "1) First use listing tools to get an overview (e.g., cpg_list_functions for function summaries with names, parameters, and callees). " + "2) Then use cpg_get_node with specific IDs to inspect the actual source code of functions that look relevant. " + "Do NOT stop at summaries alone — always retrieve and inspect the code before drawing conclusions. " + + "When inspecting a node with cpg_get_node, the result includes prevDFG/nextDFG edges showing data flow connections. " + + "For questions like 'where does this value come from?', use cpg_dfg_backward with the node ID to traverse the DFG backwards and find data sources. " + "If you can answer from previous tool results already in the conversation, respond directly without calling tools again. " + + "If a tool call fails, do NOT retry the same call — instead, answer the question using the information you already have from previous tool results and your own knowledge. Never tell the user to do the analysis themselves. " + "Explain your findings clearly." suspend fun readSseStream(channel: ByteReadChannel, processLine: suspend (String) -> Unit) { diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/WelcomeScreen.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/WelcomeScreen.svelte index c33d46bb97d..9895b83d9ed 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/WelcomeScreen.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/WelcomeScreen.svelte @@ -11,10 +11,10 @@ // Suggested questions - project-wide const suggestedQuestions = [ - 'List all functions in the project', - 'Find security vulnerabilities', - 'Analyze data flows from user inputs', - 'Show all external dependencies' + 'List all functions and their parameters', + 'What classes are defined?', + 'Which function implements AES encryption?', + 'What functions are called by the main entry point?' ]; function handleSendMessage() { diff --git a/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte index 703a4ca76ea..48696bf2a09 100644 --- a/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte +++ b/codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte @@ -91,7 +91,9 @@ try { const llmMessages: LLMMessage[] = chatMessages.map((msg) => ({ role: msg.role as 'user' | 'assistant', - content: msg.content + content: msg.contentType === 'tool-result' && msg.toolResult + ? `[Tool: ${msg.toolResult.toolName}]\n${typeof msg.toolResult.content === 'string' ? msg.toolResult.content : JSON.stringify(msg.toolResult.content)}` + : msg.content })); const callbacks: StreamingCallbacks = { diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt index 5e68d984504..f20c0e54894 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt @@ -37,9 +37,9 @@ fun configureServer( // this.addListPasses() // this.addRunPass() // this.addCpgAnalyzeTool() - this.addCpgLlmAnalyzeTool() - this.addCpgApplyConceptsTool() - this.addCpgDataflowTool() + // this.addCpgLlmAnalyzeTool() + // this.addCpgApplyConceptsTool() + // this.addCpgDataflowTool() this.listFunctions() this.listRecords() this.listCalls() @@ -50,6 +50,7 @@ fun configureServer( // this.getArgByIndexOrName() // this.listConceptsAndOperations() this.getNode() + this.addDfgBackwardTool() this } ): Server { diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDfgBackwardTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDfgBackwardTool.kt new file mode 100644 index 00000000000..679cc1c359b --- /dev/null +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDfgBackwardTool.kt @@ -0,0 +1,116 @@ +/* + * 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.cpg.mcp.mcpserver.tools + +import de.fraunhofer.aisec.cpg.TranslationResult +import de.fraunhofer.aisec.cpg.graph.Backward +import de.fraunhofer.aisec.cpg.graph.GraphToFollow +import de.fraunhofer.aisec.cpg.graph.Interprocedural +import de.fraunhofer.aisec.cpg.graph.nodes +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgIdPayload +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.runOnCpg +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toObject +import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toQueryTreeNode +import de.fraunhofer.aisec.cpg.query.May +import de.fraunhofer.aisec.cpg.query.dataFlow +import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest +import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult +import io.modelcontextprotocol.kotlin.sdk.types.TextContent +import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema +import kotlin.uuid.Uuid +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject + +fun Server.addDfgBackwardTool() { + val toolDescription = + """ + Traverse the Data Flow Graph (DFG) backwards from a given node to find where data originates. + + Uses the CPG's built-in dataflow analysis with backward direction to trace data sources. + The analysis follows prevDFG edges and stops at nodes that have no further incoming data flows (= data sources). + + Use this to answer questions like "where does this value come from?" or "what is the source of this data?". + + Parameters: + - id: The ID of the node to start the backward traversal from. + """ + .trimIndent() + + val inputSchema = + ToolSchema( + properties = + buildJsonObject { + putJsonObject("id") { + put("type", "string") + put( + "description", + "The ID of the node to start the backward DFG traversal from.", + ) + } + }, + required = listOf("id"), + ) + + this.addTool( + name = "cpg_dfg_backward", + description = toolDescription, + inputSchema = inputSchema, + ) { request -> + request.runOnCpg { result: TranslationResult, request: CallToolRequest -> + val payload = + request.arguments?.toObject() + ?: return@runOnCpg CallToolResult( + content = + listOf( + TextContent("Invalid or missing payload for cpg_dfg_backward tool.") + ) + ) + + val startId = Uuid.parse(payload.id) + val startNode = + result.nodes.find { it.id == startId } + ?: return@runOnCpg CallToolResult( + content = listOf(TextContent("No node found with ID ${payload.id}")) + ) + + val queryTree = + dataFlow( + startNode = startNode, + direction = Backward(GraphToFollow.DFG), + type = May, + scope = Interprocedural(), + predicate = { it.prevDFG.isEmpty() }, + ) + + CallToolResult( + content = listOf(TextContent(Json.encodeToString(queryTree.toQueryTreeNode()))) + ) + } + } +} diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt index cced313f596..b7806d7cb92 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Serializable.kt @@ -58,6 +58,8 @@ data class OverlayInfo( ) } +@Serializable data class ParameterInfo(val id: String, val name: String, val type: String) + @Serializable data class FunctionSummary( val id: String, @@ -65,7 +67,7 @@ data class FunctionSummary( val fileName: String?, val startLine: Int?, val endLine: Int?, - val parameters: List, + val parameters: List, val returnType: String?, val callees: List, val code: String?, @@ -112,7 +114,7 @@ data class DataflowResult( @Serializable data class QueryTreeNode( - val id: String, + val queryTreeId: String, val value: String, val node: NodeJSON?, val children: List = emptyList(), diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt index 1c412671640..5868528fefb 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt @@ -49,7 +49,7 @@ import kotlinx.serialization.json.JsonObject fun QueryTree.toQueryTreeNode(): QueryTreeNode { return QueryTreeNode( - id = this.id.toString(), + queryTreeId = this.id.toString(), value = this.value.toString(), node = this.node?.toJSON(noEdges = true), children = this.children.map { it.toQueryTreeNode() }, @@ -68,7 +68,14 @@ fun FunctionDeclaration.toSummary() = fileName = this.location?.artifactLocation?.fileName, startLine = this.location?.region?.startLine, endLine = this.location?.region?.endLine, - parameters = this.parameters.map { "${it.type.typeName} ${it.name.localName}" }, + parameters = + this.parameters.map { + ParameterInfo( + id = it.id.toString(), + name = it.name.localName, + type = it.type.typeName, + ) + }, returnType = this.returnTypes.firstOrNull()?.typeName, callees = this.callees.map { it.name.localName }, code = this.code, diff --git a/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt b/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt index b8ff157e4a1..120c208ec6a 100644 --- a/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt +++ b/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/CpgAnalyzeToolTest.kt @@ -63,6 +63,7 @@ class CpgAnalyzeToolTest { "cpg_list_available_concepts", "cpg_list_available_operations", "cpg_get_node", + "cpg_dfg_backward", ), testServer.tools.keys, ) diff --git a/cpg-serialization/src/main/kotlin/de/fraunhofer/aisec/cpg/serialization/NodeSerialization.kt b/cpg-serialization/src/main/kotlin/de/fraunhofer/aisec/cpg/serialization/NodeSerialization.kt index 18c8792052f..46cb1979777 100644 --- a/cpg-serialization/src/main/kotlin/de/fraunhofer/aisec/cpg/serialization/NodeSerialization.kt +++ b/cpg-serialization/src/main/kotlin/de/fraunhofer/aisec/cpg/serialization/NodeSerialization.kt @@ -70,9 +70,9 @@ data class NodeJSON( val endColumn: Int, val code: String, val name: String, - // val astChildren: List, - // val prevDFG: List = emptyList(), - // val nextDFG: List = emptyList(), + // val astChildren: List, + val prevDFG: List = emptyList(), + val nextDFG: List = emptyList(), @Serializable(with = UuidSerializer::class) val translationUnitId: Uuid? = null, val componentName: String? = null, val fileName: String? = null, @@ -95,11 +95,12 @@ fun Node.toJSON(noEdges: Boolean = false): NodeJSON { val path = uri.toString() path.substringAfterLast('/').substringAfterLast('\\') }, - // astChildren = - // if (noEdges) emptyList() - // else (this as? AstNode)?.astChildren?.map { it.toJSON() } ?: emptyList(), - // prevDFG = if (noEdges) emptyList() else this.prevDFGEdges.map { it.toJSON() }, - // nextDFG = if (noEdges) emptyList() else this.nextDFGEdges.map { it.toJSON() }, + // astChildren = + // if (noEdges) emptyList() + // else (this as? AstNode)?.astChildren?.map { it.toJSON() } ?: + // emptyList(), + prevDFG = if (noEdges) emptyList() else this.prevDFGEdges.map { it.toJSON() }, + nextDFG = if (noEdges) emptyList() else this.nextDFGEdges.map { it.toJSON() }, translationUnitId = this.translationUnit?.id, componentName = this.component?.name?.toString(), ) From 46e0259e9748566a685515e0797e088882670751 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Wed, 18 Feb 2026 14:02:44 +0100 Subject: [PATCH 56/93] Minor changes --- .../ai-agent/widgets/DfgFlowWidget.svelte | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/DfgFlowWidget.svelte diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/DfgFlowWidget.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/DfgFlowWidget.svelte new file mode 100644 index 00000000000..afa54c6a1a9 --- /dev/null +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/widgets/DfgFlowWidget.svelte @@ -0,0 +1,65 @@ + + +{#if nodes.length === 0} +
No data flow found.
+{:else} +
+
+ + + + + Data flow (backward) +
+ +
+ {#each nodes as node, i} + {#if i > 0} + ← + {/if} +
+
+ {node.type ?? ''} + {#if node.startLine != null && node.startLine >= 0} + Ā· + + {node.fileName ? `${node.fileName}:${node.startLine}` : `:${node.startLine}`} + + {/if} +
+
{node.name ?? '?'}
+ {#if node.code && node.code !== node.name} +
{node.code.trim()}
+ {/if} +
+ {/each} +
+
+{/if} \ No newline at end of file From e6191cec293f2c7176227bdcc8c4c234f1f2281e Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Wed, 18 Feb 2026 14:19:33 +0100 Subject: [PATCH 57/93] Minor changes 2 --- .../de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt | 8 ++++++-- .../src/lib/components/ai-agent/ChatInterface.svelte | 5 ++++- .../aisec/cpg/mcp/mcpserver/tools/CpgDfgBackwardTool.kt | 5 +++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt index c0c3dedc61f..f17b7d65908 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt @@ -110,9 +110,13 @@ class ChatClient( onText = { text -> send(Events.text(text)) }, onReasoning = { thought -> send(Events.reasoning(thought)) }, ) + println( + "Agent- Initial sendPrompt returned ${toolCalls.size} tool calls: ${toolCalls.map { it.name }}" + ) while (toolCalls.isNotEmpty() && counter < maxToolIterations) { counter++ + println("Agent- Round $counter: executing ${toolCalls.map { it.name }}") val roundtripResults = toolCalls.map { toolCall -> val result = executeToolCall(toolCall) { jsonEvent -> send(jsonEvent) } @@ -131,7 +135,7 @@ class ChatClient( ) } } catch (e: Exception) { - println("[LLM] Error: ${e.message}") + println("ChatClient.chat - Error: ${e.message}") send(Events.text("Error: ${e.message}")) } } @@ -151,7 +155,7 @@ class ChatClient( val content = buildToolContentPayload(contentTexts) val event = Events.toolResult(toolCall.name, content) - println("[Tool] Emitting event: $event") + println("Tool - Emitting event: $event") emit(event) resultText diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte index a19162aab8b..198dbadc001 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte @@ -2,6 +2,7 @@ import MarkdownRenderer from './MarkdownRenderer.svelte'; import MessageInput from './MessageInput.svelte'; import CodeItemList, { isCodeItemContent } from './widgets/CodeItemList.svelte'; + import DfgFlowWidget from './widgets/DfgFlowWidget.svelte'; import CodePreview from './CodePreview.svelte'; import ApiService from '$lib/services/apiService'; import type { NodeJSON, AnalysisResultJSON, TranslationUnitJSON, ChatMessage } from '$lib/types'; @@ -200,7 +201,9 @@
{/if} {#if message.contentType === 'tool-result' && message.toolResult} - {#if isCodeItemContent(message.toolResult.content)} + {#if message.toolResult.toolName === 'cpg_dfg_backward'} + + {:else if isCodeItemContent(message.toolResult.content)} {:else} diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDfgBackwardTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDfgBackwardTool.kt index 679cc1c359b..ff2809261be 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDfgBackwardTool.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgDfgBackwardTool.kt @@ -53,9 +53,10 @@ fun Server.addDfgBackwardTool() { Traverse the Data Flow Graph (DFG) backwards from a given node to find where data originates. Uses the CPG's built-in dataflow analysis with backward direction to trace data sources. - The analysis follows prevDFG edges and stops at nodes that have no further incoming data flows (= data sources). + The analysis follows prevDFG edges and stops at nodes that have no further incoming data flows. - Use this to answer questions like "where does this value come from?" or "what is the source of this data?". + Example usage: + - "Where does this value come from?" Parameters: - id: The ID of the node to start the backward traversal from. From f8221bbe699e6fc2fae700c6f96fd4fa987322cc Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Wed, 18 Feb 2026 14:20:26 +0100 Subject: [PATCH 58/93] Set noEdges to false for query tree --- .../de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt index 5868528fefb..fd3c9dd6df2 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Util.kt @@ -51,7 +51,7 @@ fun QueryTree.toQueryTreeNode(): QueryTreeNode { return QueryTreeNode( queryTreeId = this.id.toString(), value = this.value.toString(), - node = this.node?.toJSON(noEdges = true), + node = this.node?.toJSON(noEdges = false), children = this.children.map { it.toQueryTreeNode() }, ) } From 302b01f7e33d4c16f5624bfe09960f3ba725dcf0 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Wed, 18 Feb 2026 16:49:42 +0100 Subject: [PATCH 59/93] Remove debug logs --- .../codyze/console/ai/clients/OpenAiClient.kt | 25 +++++-------------- .../aisec/codyze/console/ai/clients/Util.kt | 10 ++++---- .../mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt | 12 +++++---- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt index 50ed4a6b939..c01264dd752 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt @@ -177,17 +177,6 @@ class OpenAiClient( val request = OpenAiRequest(model = model, messages = messages, tools = openAiTools, stream = true) - val msgChars = messages.sumOf { (it.content as? JsonPrimitive)?.content?.length ?: 0 } - val toolChars = - openAiTools?.sumOf { it.function.description.length + it.function.name.length } ?: 0 - val systemChars = SYSTEM_PROMPT.length - val totalChars = msgChars + toolChars + systemChars - println( - "Sending request: Messages: ${messages.size} | Tools: ${openAiTools?.size ?: 0} | ~${totalChars / 4} tokens" - ) - val requestJson = Json { prettyPrint = true }.encodeToString(request) - println("Request:\n$requestJson") - httpClient .preparePost("$baseUrl/v1/chat/completions") { contentType(ContentType.Application.Json) @@ -260,9 +249,9 @@ class OpenAiClient( } } - delta["tool_calls"]?.jsonArray?.forEach { tcElement -> - val tc = tcElement.jsonObject - val index = tc["index"]?.jsonPrimitive?.intOrNull ?: 0 + delta["tool_calls"]?.jsonArray?.forEach { tool -> + val toolJSON = tool.jsonObject + val index = toolJSON["index"]?.jsonPrimitive?.intOrNull ?: 0 val toolCall = toolCallsMap.getOrPut(index) { mutableMapOf( @@ -273,9 +262,9 @@ class OpenAiClient( ) } - tc["id"]?.jsonPrimitive?.contentOrNull?.let { toolCall["id"] = it } - tc["type"]?.jsonPrimitive?.contentOrNull?.let { toolCall["type"] = it } - tc["function"]?.jsonObject?.let { func -> + toolJSON["id"]?.jsonPrimitive?.contentOrNull?.let { toolCall["id"] = it } + toolJSON["type"]?.jsonPrimitive?.contentOrNull?.let { toolCall["type"] = it } + toolJSON["function"]?.jsonObject?.let { func -> func["name"]?.jsonPrimitive?.contentOrNull?.let { toolCall["name"] = it totalOutputChars += it.length @@ -289,8 +278,6 @@ class OpenAiClient( } } - println("Response finished | ~${totalOutputChars / 4} tokens") - toolCallsMap.values.forEach { toolCall -> if (toolCall["name"] != null) { accumulatedToolCalls.add( diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt index d9b67d59088..3153cbe5172 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/Util.kt @@ -34,15 +34,15 @@ import kotlinx.serialization.json.put const val SYSTEM_PROMPT = "You are a code analysis assistant with access to CPG (Code Property Graph) tools. " + "You have ONLY the tools listed in the tool definitions — do NOT invent or guess tool names. " + - "When the user asks a question about the code, use your tools immediately — never ask for permission or confirmation, never ask the user to do the analysis themselves. Act directly. " + + "When the user asks a question about the code, use your tools immediately do not ask for permission or confirmation and also don't ask the user " + + "to do the analysis themselves. " + "Follow a multi-step approach: " + - "1) First use listing tools to get an overview (e.g., cpg_list_functions for function summaries with names, parameters, and callees). " + + "1) First you can use listing tools to get an overview (e.g., cpg_list_functions for function summaries with names, parameters, and callees). " + "2) Then use cpg_get_node with specific IDs to inspect the actual source code of functions that look relevant. " + "Do NOT stop at summaries alone — always retrieve and inspect the code before drawing conclusions. " + "When inspecting a node with cpg_get_node, the result includes prevDFG/nextDFG edges showing data flow connections. " + - "For questions like 'where does this value come from?', use cpg_dfg_backward with the node ID to traverse the DFG backwards and find data sources. " + - "If you can answer from previous tool results already in the conversation, respond directly without calling tools again. " + - "If a tool call fails, do NOT retry the same call — instead, answer the question using the information you already have from previous tool results and your own knowledge. Never tell the user to do the analysis themselves. " + + "If you can answer from previous tool results already in the conversation, you can response without calling tools again. " + + "If a tool call fails, do NOT retry the same call — instead, answer the question using the information you already have from previous tool results and your own knowledge. " + "Explain your findings clearly." suspend fun readSseStream(channel: ByteReadChannel, processLine: suspend (String) -> Unit) { diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt index 010c87ef235..778dd86ed73 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt @@ -245,11 +245,13 @@ fun Server.addCpgLlmAnalyzeTool() { type = node?.javaClass?.simpleName ?: suggestion.type, code = node?.code ?: suggestion.code, fileName = - node?.location?.artifactLocation?.uri?.let { uri -> - uri.toString() - .substringAfterLast('/') - .substringAfterLast('\\') - } ?: suggestion.fileName, + node + ?.location + ?.artifactLocation + ?.uri + ?.toString() + ?.substringAfterLast('/') + ?.substringAfterLast('\\') ?: suggestion.fileName, startLine = node?.location?.region?.startLine ?: suggestion.startLine, endLine = node?.location?.region?.endLine ?: suggestion.endLine, From 1f05e0f99df4bbb1b8a919b50c5048b51335326d Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 19 Feb 2026 17:24:23 +0100 Subject: [PATCH 60/93] Add missing payload for concept assignment --- .../mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt | 37 ++----------------- .../cpg/mcp/mcpserver/tools/utils/Payloads.kt | 2 + 2 files changed, 6 insertions(+), 33 deletions(-) diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt index 16e8766abfd..b3c7a778bbb 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt @@ -42,11 +42,8 @@ import io.modelcontextprotocol.kotlin.sdk.types.ModelPreferences import io.modelcontextprotocol.kotlin.sdk.types.Role import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage import io.modelcontextprotocol.kotlin.sdk.types.TextContent -import kotlinx.serialization.Serializable import kotlinx.serialization.json.* -@Serializable data class OverlaySuggestionsResponse(val items: List) - fun Server.addCpgLlmAnalyzeTool() { val toolDescription = """ @@ -162,7 +159,7 @@ fun Server.addCpgLlmAnalyzeTool() { appendLine( """ { - "items": [ + "conceptAssignments": [ { "nodeId": "1234", "overlay": "fully.qualified.class.Name", @@ -220,12 +217,9 @@ fun Server.addCpgLlmAnalyzeTool() { // Extract the LLM response val llmResponse = (result.content as? TextContent)?.text ?: "No response from LLM" - // Extract and validate JSON - val jsonStr = extractJson(llmResponse) - // Parse LLM response and enrich with further node properties try { - val parsed = Json.decodeFromString(jsonStr) + val parsed = Json.decodeFromString(llmResponse) val nodes = globalAnalysisResult?.nodes ?: emptyList() val enrichedItems = @@ -250,11 +244,11 @@ fun Server.addCpgLlmAnalyzeTool() { ) } - val response = OverlaySuggestionsResponse(items = enrichedItems) + val response = ConceptAssignmentResponse(items = enrichedItems) CallToolResult(content = listOf(TextContent(Json.encodeToString(response)))) } catch (_: Exception) { // If parsing fails, return the extracted JSON anyway - CallToolResult(content = listOf(TextContent(jsonStr))) + CallToolResult(content = listOf(TextContent(llmResponse))) } } } catch (e: Exception) { @@ -267,26 +261,3 @@ fun Server.addCpgLlmAnalyzeTool() { } } } - -/** - * Extract JSON from LLM response that might be wrapped in markdown code blocks or contain extra - * text - */ -private fun extractJson(response: String): String { - val trimmed = response.trim() - - val fencedMatch = Regex("(?s)```(?:json)?\\s*(\\{.*?\\})\\s*```").find(trimmed) - if (fencedMatch != null) { - return fencedMatch.groupValues[1] - } - - // Try to extract JSON between first { and last } - val firstBrace = trimmed.indexOf('{') - val lastBrace = trimmed.lastIndexOf('}') - if (firstBrace >= 0 && lastBrace > firstBrace) { - return trimmed.substring(firstBrace, lastBrace + 1) - } - - // Return as-is if no extraction worked - return trimmed -} diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt index 5f2ec0354e2..8267f66052f 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt @@ -95,6 +95,8 @@ data class ConceptAssignment( val endLine: Int? = null, ) +@Serializable data class ConceptAssignmentResponse(val items: List) + /** * This class represents the payload for a CPG data flow analysis request, containing the source and * target concept types. From 912e91eaedb0a99160f54d68eae82b255cf2e3a4 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 20 Feb 2026 10:36:07 +0100 Subject: [PATCH 61/93] Revert CLI run command back to clikt and add wait parameter --- codyze-console/src/main/webapp/pnpm-lock.yaml | 7 ++++ .../fraunhofer/aisec/cpg/mcp/Application.kt | 35 ++++++++++--------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/codyze-console/src/main/webapp/pnpm-lock.yaml b/codyze-console/src/main/webapp/pnpm-lock.yaml index 4e87535da7e..7286c9540c5 100644 --- a/codyze-console/src/main/webapp/pnpm-lock.yaml +++ b/codyze-console/src/main/webapp/pnpm-lock.yaml @@ -1095,6 +1095,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + mini-svg-data-uri@1.4.4: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true @@ -2294,6 +2299,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@16.4.2: {} + mini-svg-data-uri@1.4.4: {} minimatch@3.1.2: diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt index a82f3acd9c5..d426ae104af 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/Application.kt @@ -25,6 +25,10 @@ */ package de.fraunhofer.aisec.cpg.mcp +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.main +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.int import de.fraunhofer.aisec.cpg.mcp.mcpserver.configureServer import io.ktor.server.cio.* import io.ktor.server.engine.* @@ -36,24 +40,21 @@ import kotlinx.coroutines.runBlocking import kotlinx.io.asSink import kotlinx.io.asSource import kotlinx.io.buffered -import picocli.CommandLine -@CommandLine.Command(name = "cpg-mcp") -class Application : Runnable { - @CommandLine.Option( - names = ["--sse"], - description = - [ - "Provide the port to run SSE (Server Sent Events). If not specified, the MCP server will run using stdio." - ], - ) - var ssePort: Int? = null +class Application : CliktCommand(name = "cpg-mcp") { + private val ssePort by + option( + "--sse", + help = + "Provide the port to run SSE (Server Sent Events). If not specified, the MCP server will run using stdio.", + ) + .int() override fun run() { val port = ssePort if (port != null) { println("Starting MCP server in SSE mode on port $port...") - runBlocking { runSseMcpServerUsingKtorPlugin(port, configureServer()) } + runSseMcpServerUsingKtorPlugin(port, configureServer(), wait = true) } else { println("Starting MCP server in stdio mode...") runMcpServerUsingStdio() @@ -62,7 +63,7 @@ class Application : Runnable { } fun main(args: Array) { - CommandLine(Application()).execute(*args) + Application().main(args) } fun runMcpServerUsingStdio() { @@ -72,7 +73,7 @@ fun runMcpServerUsingStdio() { runBlocking { val job = Job() server.onClose { job.complete() } - server.connect(transport) + server.createSession(transport) job.join() } } @@ -83,7 +84,9 @@ fun runMcpServerUsingStdio() { * The url can be accessed in the MCP inspector at [http://localhost:$port] * * @param port The port number on which the SSE MCP server will listen for client connections. + * @param wait If true the thread is blocked until the server stops. This flag is needed when the + * server runs in the background alongside another server (e.g. in codyze-console). */ -fun runSseMcpServerUsingKtorPlugin(port: Int, server: Server) { - embeddedServer(CIO, host = "0.0.0.0", port = port) { mcp { server } }.start(wait = false) +fun runSseMcpServerUsingKtorPlugin(port: Int, server: Server, wait: Boolean = false) { + embeddedServer(CIO, host = "0.0.0.0", port = port) { mcp { server } }.start(wait = wait) } From d353e6aff3ea1bcf6191066d07a2b4b98aad6a2d Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 20 Feb 2026 10:55:27 +0100 Subject: [PATCH 62/93] Downgrade ktor to 3.2.3 due to kotlin-mcp-sdk --- .../de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt | 1 + gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt index a119038b41c..0e55af2a7f7 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/ListCommands.kt @@ -38,6 +38,7 @@ import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.addTool import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.runOnCpg import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toJson import io.modelcontextprotocol.kotlin.sdk.server.Server +import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3d37144e726..ccae2780f91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ slf4j = "2.0.16" clikt = "5.1.0" kaml = "0.104.0" sarif4k = "0.6.0" -ktor = "3.4.0" +ktor = "3.2.3" node = "22.14.0" deno-plugin = "0.1.5" From b132d8127905eed9043ca53636e8cb19d815b204 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 20 Feb 2026 17:26:39 +0100 Subject: [PATCH 63/93] Change cpg_llm_analyze tool in prompt --- .../aisec/cpg/mcp/mcpserver/McpServer.kt | 4 +- .../mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt | 355 +++++++----------- .../cpg/mcp/mcpserver/tools/utils/Payloads.kt | 8 - 3 files changed, 139 insertions(+), 228 deletions(-) diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt index 8e25cea9f1a..3d923ddada7 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/McpServer.kt @@ -33,11 +33,11 @@ import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities fun configureServer( configure: Server.() -> Server = { + // TOOLS this.addCpgTranslate() this.addListPasses() this.addRunPass() this.addCpgAnalyzeTool() - this.addCpgLlmAnalyzeTool() this.addCpgApplyConceptsTool() this.addCpgDataflowTool() this.listFunctions() @@ -51,6 +51,8 @@ fun configureServer( this.listConceptsAndOperations() this.getNode() this.addDfgBackwardTool() + // PROMPTS + this.addSuggestConceptsPrompt() this } ): Server { diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt index b3c7a778bbb..e46bdb22c3f 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/CpgLlmAnalyzeTool.kt @@ -25,239 +25,156 @@ */ package de.fraunhofer.aisec.cpg.mcp.mcpserver.tools -import de.fraunhofer.aisec.cpg.graph.nodes import de.fraunhofer.aisec.cpg.mcp.mcpserver.cpgDescription -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.* -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.CpgLlmAnalyzePayload import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.getAvailableConcepts import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.getAvailableOperations -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toObject -import de.fraunhofer.aisec.cpg.mcp.mcpserver.tools.utils.toSchema -import de.fraunhofer.aisec.cpg.serialization.toJSON import io.modelcontextprotocol.kotlin.sdk.server.Server -import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult -import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequest -import io.modelcontextprotocol.kotlin.sdk.types.CreateMessageRequestParams -import io.modelcontextprotocol.kotlin.sdk.types.ModelPreferences +import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult +import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument +import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage import io.modelcontextprotocol.kotlin.sdk.types.Role -import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage import io.modelcontextprotocol.kotlin.sdk.types.TextContent -import kotlinx.serialization.json.* -fun Server.addCpgLlmAnalyzeTool() { - val toolDescription = - """ - Generate a prompt asking the LLM to suggest concepts/operations. - - This tool creates a detailed prompt that asks the LLM to act as a software engineer with expertise in software security - and suggest appropriate concepts and operations for the analyzed code. - After using this tool, review the suggestions before applying them with cpg_apply_concepts. - """ - .trimIndent() - - this.addTool( - name = "cpg_llm_analyze", - description = toolDescription, - inputSchema = CpgLlmAnalyzePayload::class.toSchema(), - ) { request -> - try { - val payload = - if (request.arguments.isNullOrEmpty()) { - CpgLlmAnalyzePayload() - } else { - request.arguments?.toObject() ?: CpgLlmAnalyzePayload() - } - - val hasAnalysisResult = globalAnalysisResult != null - val availableConcepts = getAvailableConcepts() - val availableOperations = getAvailableOperations() - - val prompt = buildString { - appendLine("# Code Analysis") - appendLine() - appendLine( - "You are a Software engineer with expertise in software security for more than 10 years.\n" - ) - appendLine() - - appendLine("## About CPG") - appendLine(cpgDescription) - appendLine() - - appendLine("## Goal") - appendLine( - "Mark interesting data and operations so we can analyze how data flows through code to discover patterns." +fun Server.addSuggestConceptsPrompt() { + this.addPrompt( + name = "suggest_concepts", + description = + "Generates a prompt that guides the LLM to suggest CPG concepts and operations for the analyzed code. Use this before applying concepts with cpg_apply_concepts.", + arguments = + listOf( + PromptArgument( + name = "description", + description = + "Optional additional context or focus area for the analysis, e.g. 'focus on authentication handling' or 'look for encryption handling'.", + required = false, ) - appendLine() - - appendLine("## Understanding Concepts and Operations") - appendLine() - appendLine("**Concepts** mark 'what something IS':") - appendLine("- Applied to data-holding nodes (variables, fields, return values)") - appendLine("- Examples: user_email → Data, api_token → Secret") - appendLine("- Purpose: Track where important data is stored") - appendLine() - appendLine("**Operations** mark 'what something DOES':") - appendLine("- Applied to nodes that perform actions (function calls, method calls)") - appendLine( - "- Examples: requests.post() → HttpRequest, file.write() → FileWrite, encrypt() → Encryption" - ) - appendLine("- Purpose: Track what happens to important data") - appendLine() - - appendLine("## Rules") - appendLine( - "1. **Domain consistency**: When using Operations, they must match their Concept's domain (e.g., GetSecret needs Secret, HttpRequest needs HttpClient)" - ) - appendLine( - "2. **Operations always need Concepts**: Every Operation MUST have a conceptNodeId pointing to an existing Concept node. However, Concepts can stand alone without Operations." - ) - appendLine() - - appendLine("## Your Task") - appendLine("1. Analyze each node for relevance") - appendLine("2. Suggest appropriate overlays using fully qualified class names") - appendLine("3. Ensure concept-operation pairs belong to the same domain") - appendLine() - appendLine( - "**IMPORTANT:** Use only existing CPG concepts/operations from the list below." - ) - appendLine( - "For additional context, you can check docstrings in the Fraunhofer CPG repository on GitHub, especially the cpg-concepts module." - ) - appendLine() + ), + ) { request -> + val description = request.arguments?.get("description") + val availableConcepts = getAvailableConcepts() + val availableOperations = getAvailableOperations() + + GetPromptResult( + description = + "Guides the LLM to explore the CPG and suggest concept/operation overlays for security-relevant nodes.", + messages = + listOf( + PromptMessage( + role = Role.User, + content = + TextContent( + text = + buildString { + appendLine("## About CPG") + appendLine(cpgDescription) + appendLine() + + appendLine("## Goal") + appendLine( + "Identify nodes in the CPG that represent security-relevant data or operations, and suggest appropriate concept/operation overlays for them." + ) + appendLine( + "This allows us to analyze how sensitive data flows through the code and discover security-relevant patterns." + ) + appendLine() - appendLine("Available concepts:") - availableConcepts.forEach { appendLine("- ${it.name}") } - appendLine() - appendLine("Available operations:") - availableOperations.forEach { appendLine("- ${it.name}") } - appendLine() + appendLine("## Understanding Concepts and Operations") + appendLine() + appendLine("**Concepts** mark 'what something IS':") + appendLine( + "- Applied to data-holding nodes (variables, fields)" + ) + appendLine( + "- Examples: user_email → Data, api_token → Secret" + ) + appendLine( + "- Purpose: Track where important data is stored" + ) + appendLine() + appendLine("**Operations** mark 'what something DOES':") + appendLine( + "- Applied to nodes that perform actions (function calls, method calls)" + ) + appendLine( + "- Examples: requests.post() → HttpRequest, file.write() → FileWrite, encrypt() → Encryption" + ) + appendLine( + "- Purpose: Track what happens to important data" + ) + appendLine() - if (payload.description != null) { - appendLine("## Additional Context") - appendLine(payload.description) - appendLine() - } + appendLine("## Rules") + appendLine( + "1. **Domain consistency**: Operations must match their Concept's domain (e.g., GetSecret needs Secret, HttpRequest needs HttpClient)" + ) + appendLine( + "2. **Operations always need Concepts**: Every Operation MUST have a conceptNodeId pointing to an existing Concept node. Concepts can stand alone." + ) + appendLine( + "3. **Use only existing overlays**: Only use the concepts and operations listed below." + ) + appendLine() + + appendLine("## Available Concepts") + availableConcepts.forEach { appendLine("- ${it.name}") } + appendLine() + appendLine("## Available Operations") + availableOperations.forEach { appendLine("- ${it.name}") } + appendLine() + + if (description != null) { + appendLine("## Additional Context") + appendLine(description) + appendLine() + } + + appendLine("## How to Explore the Code") + appendLine( + "You can use the tools to explore the code before making suggestions." + ) + appendLine( + "The listing tools e.g., `cpg_list_functions` or `cpg_list_records`, provide a summary of functions or classes with the name, parameters and code." + ) + appendLine( + "To inspect interesting nodes in more detail use `cpg_get_node`. This retrieves the complete details of a specific node filtered by its ID." + ) - if (hasAnalysisResult) { - appendLine("## Nodes to Analyze") - appendLine("Here are all the nodes from the CPG analysis:") - appendLine("```json") - val nodes = globalAnalysisResult.nodes - nodes.forEach { node -> - appendLine(Json.encodeToString(node.toJSON(noEdges = true))) - } - appendLine("```") - } else { - appendLine("## No Analysis Available") - appendLine("No CPG analysis available. Analyze a file first using cpg_analyze.") - } - appendLine() - appendLine("## Expected Response Format") - appendLine("Respond with a JSON object in this exact format:") - appendLine( - """ - { - "conceptAssignments": [ + appendLine("## Expected Response Format") + appendLine( + "After exploring the code, respond with a JSON object in this exact format:" + ) + appendLine( + """ { - "nodeId": "1234", - "overlay": "fully.qualified.class.Name", - "overlayType": "Concept" | "Operation", - "conceptNodeId": "string (REQUIRED for operations)", - "reasoning": "Security reasoning for this classification", - "securityImpact": "Potential security implications" + "conceptAssignments": [ + { + "nodeId": "1234", + "overlay": "fully.qualified.class.Name", + "overlayType": "Concept", + "reasoning": "Reason for this classification", + "securityImpact": "Potential security implications" + }, + { + "nodeId": "5678", + "overlay": "fully.qualified.class.Name", + "overlayType": "Operation", + "conceptNodeId": "1234", + "reasoning": "Reason for this classification", + "securityImpact": "Potential security implications" + } + ] } - ] - } - """ - .trimIndent() - ) - appendLine() - appendLine( - "**IMPORTANT**: After providing your analysis, WAIT for user approval before applying any concepts. Do not automatically execute cpg_apply_concepts." - ) - } - - // Get the session ID - val sessionId = this.sessions.values.firstOrNull()?.sessionId - - if (sessionId == null) { - CallToolResult( - content = listOf(TextContent("Error: No active client session found")), - isError = true, - ) - } else { - // Use sampling to send the prompt to the LLM via the client - val samplingRequest = - CreateMessageRequest( - params = - CreateMessageRequestParams( - messages = - listOf( - SamplingMessage( - role = Role.User, - content = TextContent(text = prompt), + """ + .trimIndent() ) - ), - systemPrompt = - "You are a software security expert analyzing code for security vulnerabilities.", - maxTokens = 4000, - modelPreferences = - ModelPreferences( - intelligencePriority = 0.9, - speedPriority = 0.5, - ), - ) - ) - - // Send sampling request to client (which will forward to LLM) - val result = this.createMessage(sessionId, samplingRequest) - - // Extract the LLM response - val llmResponse = (result.content as? TextContent)?.text ?: "No response from LLM" - - // Parse LLM response and enrich with further node properties - try { - val parsed = Json.decodeFromString(llmResponse) - val nodes = globalAnalysisResult?.nodes ?: emptyList() - - val enrichedItems = - parsed.items.map { suggestion -> - val node = nodes.find { it.id.toString() == suggestion.nodeId } - - suggestion.copy( - name = node?.name?.toString() ?: suggestion.name, - type = node?.javaClass?.simpleName ?: suggestion.type, - code = node?.code ?: suggestion.code, - fileName = - node - ?.location - ?.artifactLocation - ?.uri - ?.toString() - ?.substringAfterLast('/') - ?.substringAfterLast('\\') ?: suggestion.fileName, - startLine = - node?.location?.region?.startLine ?: suggestion.startLine, - endLine = node?.location?.region?.endLine ?: suggestion.endLine, - ) - } - - val response = ConceptAssignmentResponse(items = enrichedItems) - CallToolResult(content = listOf(TextContent(Json.encodeToString(response)))) - } catch (_: Exception) { - // If parsing fails, return the extracted JSON anyway - CallToolResult(content = listOf(TextContent(llmResponse))) - } - } - } catch (e: Exception) { - CallToolResult( - content = - listOf( - TextContent("Error generating prompt: ${e.message ?: e::class.simpleName}") + appendLine() + appendLine( + "**IMPORTANT**: After providing your suggestions, WAIT for user approval before applying anything. Do not automatically call `cpg_apply_concepts`." + ) + } + ), ) - ) - } + ), + ) } } diff --git a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt index 8267f66052f..14523d7f8da 100644 --- a/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt +++ b/cpg-mcp/src/main/kotlin/de/fraunhofer/aisec/cpg/mcp/mcpserver/tools/utils/Payloads.kt @@ -95,8 +95,6 @@ data class ConceptAssignment( val endLine: Int? = null, ) -@Serializable data class ConceptAssignmentResponse(val items: List) - /** * This class represents the payload for a CPG data flow analysis request, containing the source and * target concept types. @@ -108,12 +106,6 @@ data class CpgDataflowPayload( @Description("Target concept type (e.g., 'HttpRequest', 'CallExpression')") val to: String, ) -@Serializable -data class CpgLlmAnalyzePayload( - @Description("A special description of what to take care of while analyzing the target") - val description: String? = null -) - /** * This class represents information about a pass, including its fully qualified name (FQN), a * description, required node type, dependencies, and soft dependencies. From 9f46b96929e046efbaaad5df457197d13c8b2418 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 23 Feb 2026 11:54:23 +0100 Subject: [PATCH 64/93] Remove sampling since llm_analyze mcp tool is changed to a prompt --- .../aisec/codyze/console/ai/ChatClient.kt | 33 +--------- .../codyze/console/ai/clients/GeminiClient.kt | 48 --------------- .../codyze/console/ai/clients/LlmClient.kt | 8 --- .../codyze/console/ai/clients/OpenAiClient.kt | 60 ------------------- 4 files changed, 1 insertion(+), 148 deletions(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt index f17b7d65908..04dbc26349d 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt @@ -44,8 +44,7 @@ class ChatClient( private val mcp: McpSdkClient = McpSdkClient( clientInfo = Implementation(name = "codyze-client", version = "1.0.0"), - options = - ClientOptions(capabilities = ClientCapabilities(sampling = buildJsonObject {})), + options = ClientOptions(), ) private var tools: List = emptyList() @@ -53,41 +52,11 @@ class ChatClient( /** Connect to the MCP server via SSE */ suspend fun connect() { val transport = SseClientTransport(urlString = mcpServerUrl, client = httpClient) - registerSamplingHandler() mcp.connect(transport) val toolsResult = mcp.listTools() tools = toolsResult.tools } - /** Register handler for incoming sampling requests from the server */ - private fun registerSamplingHandler() { - mcp.setRequestHandler(Method.Defined.SamplingCreateMessage) { - request, - _ -> - try { - val llmResponse = - llm.query( - messages = request.params.messages, - systemPrompt = request.params.systemPrompt, - ) - - CreateMessageResult( - role = Role.Assistant, - content = TextContent(text = llmResponse), - model = llm.modelName, - stopReason = StopReason.EndTurn, - ) - } catch (e: Exception) { - CreateMessageResult( - role = Role.Assistant, - content = TextContent(text = "Error: ${e.message}"), - model = llm.modelName, - stopReason = StopReason.EndTurn, - ) - } - } - } - /** Maximum number of tool call iterations before forcing a text response. */ private val maxToolIterations = 8 diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt index d7be305dabd..d2a003df7b3 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt @@ -31,8 +31,6 @@ import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.http.* import io.ktor.utils.io.* -import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage -import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.Tool import kotlinx.serialization.json.* @@ -44,36 +42,6 @@ class GeminiClient( ) : LlmClient { override val modelName: String = model - override suspend fun query( - messages: List, - systemPrompt: String?, - maxTokens: Int?, - ): String { - val request = - GeminiRequest( - systemInstruction = - systemPrompt?.let { GeminiContent(parts = listOf(GeminiPart(text = it))) }, - contents = - messages.map { msg -> - GeminiContent( - role = - if (msg.role.toString().lowercase() == "user") "user" else "model", - parts = - listOf(GeminiPart(text = (msg.content as? TextContent)?.text ?: "")), - ) - }, - ) - - val response = - httpClient.post("$baseUrl/models/$model:generateContent?key=$apiKey") { - contentType(ContentType.Application.Json) - setBody(request) - } - - val result = response.body() - return extractTextFromResponse(result) ?: "No response" - } - override suspend fun sendPrompt( userMessage: String, conversationHistory: List, @@ -222,20 +190,4 @@ class GeminiClient( } } } - - private fun extractTextFromResponse(result: JsonObject): String? { - return result["candidates"] - ?.jsonArray - ?.firstOrNull() - ?.jsonObject - ?.get("content") - ?.jsonObject - ?.get("parts") - ?.jsonArray - ?.firstOrNull() - ?.jsonObject - ?.get("text") - ?.jsonPrimitive - ?.content - } } diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt index 413316e83f0..d36506deb53 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/LlmClient.kt @@ -26,20 +26,12 @@ package de.fraunhofer.aisec.codyze.console.ai.clients import de.fraunhofer.aisec.codyze.console.ai.ChatMessageJSON -import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage import io.modelcontextprotocol.kotlin.sdk.types.Tool /** Interface abstracting the underlying LLM provider (Gemini, OpenAI, Ollama, etc.). */ interface LlmClient { val modelName: String - /** Query for MCP sampling requests. */ - suspend fun query( - messages: List, - systemPrompt: String?, - maxTokens: Int? = null, - ): String - /** * Streaming prompt execution for the chat. Calls [onText] for normal content and [onReasoning] * for thoughts/reasoning. diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt index c01264dd752..7fc80ed1431 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/OpenAiClient.kt @@ -31,8 +31,6 @@ import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.http.* import io.ktor.utils.io.* -import io.modelcontextprotocol.kotlin.sdk.types.SamplingMessage -import io.modelcontextprotocol.kotlin.sdk.types.TextContent import io.modelcontextprotocol.kotlin.sdk.types.Tool import kotlinx.serialization.json.* @@ -45,48 +43,6 @@ class OpenAiClient( override val modelName: String = model private val usesThinkTags = model.contains("glm", ignoreCase = true) - /** Query the LLM when we have a tool that uses sampling */ - override suspend fun query( - messages: List, - systemPrompt: String?, - maxTokens: Int?, - ): String { - val openAiMessages = buildList { - if (systemPrompt != null) { - add(OpenAiMessage(role = "system", content = JsonPrimitive(systemPrompt))) - } - messages.forEach { msg -> - add( - OpenAiMessage( - role = msg.role.toString().lowercase(), - content = JsonPrimitive((msg.content as? TextContent)?.text ?: ""), - ) - ) - } - } - - val queryRequest = - OpenAiRequest( - model = model, - messages = openAiMessages, - maxTokens = maxTokens, - stream = false, - ) - - val response = - httpClient.post("$baseUrl/v1/chat/completions") { - contentType(ContentType.Application.Json) - setBody(queryRequest) - } - - if (!response.status.isSuccess()) { - return "LLM request failed: ${response.status.value} ${response.status.description}" - } - - val result = response.body() - return extractContentFromResponse(result) ?: "No response" - } - override suspend fun sendPrompt( userMessage: String, conversationHistory: List, @@ -296,20 +252,4 @@ class OpenAiClient( } } } - - private fun extractContentFromResponse(result: JsonObject): String? { - val content = - result["choices"] - ?.jsonArray - ?.firstOrNull() - ?.jsonObject - ?.get("message") - ?.jsonObject - ?.get("content") - ?.jsonPrimitive - ?.content - - return if (content == null) null - else if (usesThinkTags) stripThinkTags(content) else content - } } From 352d98a41819704204fce604e897b62276f3cfdf Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Wed, 25 Feb 2026 13:38:17 +0100 Subject: [PATCH 65/93] Minor changes --- .../aisec/codyze/console/ai/ChatService.kt | 10 +++----- .../codyze/console/ai/clients/GeminiClient.kt | 24 ++++++++++--------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt index 9b3d5c5fce3..276adc8a351 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt @@ -45,12 +45,12 @@ import kotlinx.serialization.json.Json /** ChatService manages LLM client configuration and provides an API for chat interactions. */ class ChatService { + // /resources/application.conf private val config = ConfigFactory.load() private val llmProvider: String = config.getString("llm.client") - private val llmModel: String = config.getString("llm.$llmProvider.model") - private val llmBaseUrl: String = config.getString("llm.$llmProvider.baseUrl") + private val mcpServerUrl: String = config.getString("mcp.serverUrl") private val httpClient = HttpClient(CIO) { @@ -82,11 +82,7 @@ class ChatService { } private val chatClient: ChatClient = - ChatClient( - httpClient = httpClient, - llm = llmClient, - mcpServerUrl = config.getString("mcp.serverUrl"), - ) + ChatClient(httpClient = httpClient, llm = llmClient, mcpServerUrl = mcpServerUrl) suspend fun connect() { chatClient.connect() diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt index d2a003df7b3..a0c19040ffb 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/clients/GeminiClient.kt @@ -95,13 +95,13 @@ class GeminiClient( GeminiContent( role = "model", parts = - roundtrip.map { tr -> + roundtrip.map { tool -> GeminiPart( functionCall = GeminiFunctionCall( - name = tr.call.name, + name = tool.call.name, args = - Json.parseToJsonElement(tr.call.arguments) + Json.parseToJsonElement(tool.call.arguments) .jsonObject, ) ) @@ -110,13 +110,15 @@ class GeminiClient( GeminiContent( role = "user", parts = - roundtrip.map { tr -> + roundtrip.map { tool -> GeminiPart( functionResponse = GeminiFunctionResponse( - name = tr.call.name, + name = tool.call.name, response = - buildJsonObject { put("result", tr.result) }, + buildJsonObject { + put("result", tool.result) + }, ) ) }, @@ -174,15 +176,15 @@ class GeminiClient( ?.jsonArray parts?.forEach { part -> - val partObj = part.jsonObject + val partJSON = part.jsonObject - partObj["text"]?.jsonPrimitive?.contentOrNull?.let { text -> + partJSON["text"]?.jsonPrimitive?.contentOrNull?.let { text -> if (text.isNotEmpty()) onText(text) } - partObj["functionCall"]?.jsonObject?.let { fc -> - val name = fc["name"]?.jsonPrimitive?.contentOrNull - val args = fc["args"]?.jsonObject + partJSON["functionCall"]?.jsonObject?.let { functionCall -> + val name = functionCall["name"]?.jsonPrimitive?.contentOrNull + val args = functionCall["args"]?.jsonObject if (name != null) { toolCalls.add(ToolCall(name, args?.toString() ?: "{}")) } From a20f9fed683c35ff796666ca97728c8c97c605dd Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 26 Feb 2026 11:34:16 +0100 Subject: [PATCH 66/93] Change urls of mlx and vllm models --- codyze-console/src/main/resources/application.conf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codyze-console/src/main/resources/application.conf b/codyze-console/src/main/resources/application.conf index 1dc4f096f0a..a409534d290 100644 --- a/codyze-console/src/main/resources/application.conf +++ b/codyze-console/src/main/resources/application.conf @@ -1,5 +1,5 @@ llm { - client = "gemini" # Options: "ollama", "gemini", "vLLM" + client = "vLLM" # Options: "ollama", "gemini", "vLLM" gemini { baseUrl = "https://generativelanguage.googleapis.com/v1beta" @@ -12,7 +12,7 @@ llm { } mlx { - baseUrl = "http://172.31.9.131:8000" + baseUrl = "http://macmidi.netsec.aisec.fraunhofer.de:8000" model = "mlx-community/Qwen3-Next-80B-A3B-Instruct-4bit" } @@ -22,8 +22,8 @@ llm { } vLLM { - baseUrl = "http://172.31.6.131:8000" - model = "zai-org/GLM-4.5-Air-FP8" + baseUrl = "http://172.31.6.131:8001" + model = "cpatonn/Qwen3-Next-80B-A3B-Instruct-AWQ-4bit" } } From 5b5f19430745abe8a581af03f9fd47022f54eab2 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 3 Mar 2026 22:19:06 +0100 Subject: [PATCH 67/93] Add prompts and resources support --- .../fraunhofer/aisec/codyze/console/Main.kt | 51 ++-- .../fraunhofer/aisec/codyze/console/Router.kt | 76 +++--- .../aisec/codyze/console/ai/ChatClient.kt | 69 ++++- .../aisec/codyze/console/ai/ChatService.kt | 5 + .../aisec/codyze/console/ai/McpModels.kt | 65 +++++ .../src/main/resources/application.conf | 4 +- .../components/ai-agent/ChatInterface.svelte | 59 +++-- .../ai-agent/McpCapabilitiesModal.svelte | 207 +++++++++++++++ .../components/ai-agent/MessageInput.svelte | 138 ++++++++-- .../components/ai-agent/WelcomeScreen.svelte | 32 ++- .../src/lib/components/ai-agent/index.ts | 4 +- .../lib/components/navigation/Sidebar.svelte | 2 +- .../src/main/webapp/src/lib/types.ts | 37 +++ .../webapp/src/routes/ai-agent/+page.svelte | 237 ----------------- .../main/webapp/src/routes/chat/+page.svelte | 246 ++++++++++++++++++ .../src/routes/{ai-agent => chat}/+page.ts | 24 +- 16 files changed, 890 insertions(+), 366 deletions(-) create mode 100644 codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpModels.kt create mode 100644 codyze-console/src/main/webapp/src/lib/components/ai-agent/McpCapabilitiesModal.svelte delete mode 100644 codyze-console/src/main/webapp/src/routes/ai-agent/+page.svelte create mode 100644 codyze-console/src/main/webapp/src/routes/chat/+page.svelte rename codyze-console/src/main/webapp/src/routes/{ai-agent => chat}/+page.ts (50%) diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt index ef19f4ddff9..d9a51d2265f 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Main.kt @@ -44,30 +44,14 @@ import kotlinx.serialization.json.Json * the [configureWebconsole] function. */ fun ConsoleService.startConsole(host: String = "localhost", port: Int = 8080) { - var chatService: ChatService? = null - - // Start MCP server in background if enabled - if (McpServerHelper.isEnabled) { - runBlocking { - McpServerHelper.startMcpServer(8081) - - val translationResult = - this@startConsole.getTranslationResult()?.analysisResult?.translationResult - if (translationResult != null) { - McpServerHelper.setGlobalAnalysisResult(translationResult) - } - - // Initialize ChatService (with MCP client) - println("Initializing ChatService...") - chatService = ChatService() - chatService.connect() - println("MCP client connected!") + val chatService: ChatService? = + if (McpServerHelper.isEnabled) { + runBlocking { initChatService() } + } else { + println("MCP module not enabled, AI chat features will be disabled") + null } - } else { - println("MCP module not enabled, AI chat features will be disabled") - } - // Start main server on port 8080 println("Starting main server on port $port...") embeddedServer(Netty, host = host, port = port) { configureWebconsole(this@startConsole, chatService) @@ -75,6 +59,21 @@ fun ConsoleService.startConsole(host: String = "localhost", port: Int = 8080) { .start(wait = true) } +private suspend fun ConsoleService.initChatService(): ChatService { + McpServerHelper.startMcpServer(8081) + + val translationResult = getTranslationResult()?.analysisResult?.translationResult + if (translationResult != null) { + McpServerHelper.setGlobalAnalysisResult(translationResult) + } + + println("Initializing ChatService...") + return ChatService().also { + it.connect() + println("MCP client connected!") + } +} + /** * This function takes care of configuring the web console based on the [service]. It sets up the * CORS policy, content negotiation, and routing. @@ -109,8 +108,12 @@ fun Application.configureWebconsole( */ fun Application.configureRouting(service: ConsoleService, chatService: ChatService? = null) { routing { - // We'll add routes here - apiRoutes(service, chatService) + apiRoutes(service) + // If the cpg-mcp module is enabled, chatService won't be null, so the endpoints will be + // reachable + if (chatService != null) { + chatRoutes(chatService) + } frontendRoutes() } } diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt index 713147d4b9c..3d63cf71c60 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/Router.kt @@ -62,7 +62,7 @@ import kotlin.reflect.KClass * - POST `/api/concept`: Adds a concept node to the current * [de.fraunhofer.aisec.codyze.AnalysisResult] */ -fun Routing.apiRoutes(service: ConsoleService, chatService: ChatService?) { +fun Routing.apiRoutes(service: ConsoleService) { // The API routes are prefixed with /api route("/api") { // The endpoint to analyze a project @@ -251,41 +251,6 @@ fun Routing.apiRoutes(service: ConsoleService, chatService: ChatService?) { // Feature flags endpoint get("/features") { call.respond(mapOf("mcpEnabled" to McpServerHelper.isEnabled)) } - // Chat endpoint - only available if MCP module is enabled - if (chatService != null) { - post("/chat") { - val request = call.receive() - call.respondTextWriter(contentType = ContentType.Text.EventStream) { - try { - chatService.chat(request).collect { chunk -> - try { - chunk.split("\n").forEach { line -> write("data: $line\n") } - write("\n") // Empty line marks end of event - flush() - } catch (e: io.ktor.utils.io.ClosedWriteChannelException) { - // Client disconnected - stop processing - throw kotlinx.coroutines.CancellationException( - "Client disconnected", - e, - ) - } - } - } catch (e: kotlinx.coroutines.CancellationException) { - // Expected when client disconnects - do nothing - } catch (e: Exception) { - // Log unexpected errors - e.printStackTrace() - try { - write("data: ERROR: ${e.message}\n\n") - flush() - } catch (ignored: Exception) { - // Channel already closed - } - } - } - } - } - // The endpoint to get a QueryTree with its parent IDs for tree expansion get("/querytrees/{queryTreeId}/parents") { val queryTreeId = @@ -305,6 +270,45 @@ fun Routing.apiRoutes(service: ConsoleService, chatService: ChatService?) { } } +/** Chat and MCP routes — only registered when [ChatService] is available. */ +fun Route.chatRoutes(chatService: ChatService) { + route("/api/chat") { + post { + val request = call.receive() + call.respondTextWriter(contentType = ContentType.Text.EventStream) { + try { + chatService.chat(request).collect { chunk -> + try { + chunk.split("\n").forEach { line -> write("data: $line\n") } + write("\n") + flush() + } catch (e: io.ktor.utils.io.ClosedWriteChannelException) { + throw kotlinx.coroutines.CancellationException("Client disconnected", e) + } + } + } catch (e: kotlinx.coroutines.CancellationException) { + // Expected when client disconnects + } catch (e: Exception) { + e.printStackTrace() + try { + write("data: ERROR: ${e.message}\n\n") + flush() + } catch (ignored: Exception) {} + } + } + } + + get("/mcp/capabilities") { call.respond(chatService.getCapabilities()) } + + post("/mcp/prompts/{name}") { + val name = + call.parameters["name"] ?: return@post call.respond(HttpStatusCode.BadRequest) + val arguments = call.receiveNullable>() ?: emptyMap() + call.respond(chatService.getPrompt(name, arguments)) + } + } +} + /** * This function sets up the static resources for the web application. It serves the static files * from the "static" directory and sets the default resource to "index.html". This serves our SPA diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt index 04dbc26349d..36ff21a4059 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatClient.kt @@ -48,13 +48,78 @@ class ChatClient( ) private var tools: List = emptyList() + private var prompts: List = emptyList() + private var resources: List = emptyList() /** Connect to the MCP server via SSE */ suspend fun connect() { val transport = SseClientTransport(urlString = mcpServerUrl, client = httpClient) mcp.connect(transport) - val toolsResult = mcp.listTools() - tools = toolsResult.tools + tools = mcp.listTools().tools + prompts = mcp.listPrompts().prompts + resources = mcp.listResources().resources + } + + /** Return all MCP capabilities */ + fun getCapabilities(): McpCapabilitiesJSON = + McpCapabilitiesJSON( + serverName = mcp.serverVersion?.name ?: "MCP Server", + serverVersion = mcp.serverVersion?.version ?: "", + tools = + tools.map { tool -> + ToolInfoJSON( + name = tool.name, + description = tool.description, + inputSchema = + ToolSchemaJSON( + properties = tool.inputSchema.properties, + required = tool.inputSchema.required, + ), + ) + }, + prompts = + prompts.map { prompt -> + PromptInfoJSON( + name = prompt.name, + description = prompt.description, + arguments = + prompt.arguments?.map { arg -> + PromptArgumentJSON( + name = arg.name, + description = arg.description, + required = arg.required, + ) + }, + ) + }, + resources = + resources.map { resource -> + ResourceInfoJSON( + uri = resource.uri, + name = resource.name, + description = resource.description, + mimeType = resource.mimeType, + ) + }, + ) + + /** Resolve a named MCP prompt and return its messages as [ChatMessageJSON]. */ + suspend fun getPrompt( + name: String, + arguments: Map = emptyMap(), + ): List { + val result = + mcp.getPrompt( + GetPromptRequest( + GetPromptRequestParams(name = name, arguments = arguments.ifEmpty { null }) + ) + ) + return result.messages.map { msg -> + ChatMessageJSON( + role = if (msg.role == Role.User) "user" else "assistant", + content = (msg.content as? TextContent)?.text ?: "", + ) + } } /** Maximum number of tool call iterations before forcing a text response. */ diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt index 276adc8a351..ab97d749056 100644 --- a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/ChatService.kt @@ -92,6 +92,11 @@ class ChatService { return chatClient.chat(request) } + fun getCapabilities(): McpCapabilitiesJSON = chatClient.getCapabilities() + + suspend fun getPrompt(name: String, arguments: Map): List = + chatClient.getPrompt(name, arguments) + fun close() { httpClient.close() } diff --git a/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpModels.kt b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpModels.kt new file mode 100644 index 00000000000..c9acceb94eb --- /dev/null +++ b/codyze-console/src/main/kotlin/de/fraunhofer/aisec/codyze/console/ai/McpModels.kt @@ -0,0 +1,65 @@ +/* + * 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.console.ai + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +@Serializable data class ToolSchemaJSON(val properties: JsonObject?, val required: List?) + +@Serializable +data class ToolInfoJSON( + val name: String, + val description: String?, + val inputSchema: ToolSchemaJSON?, +) + +@Serializable +data class PromptArgumentJSON(val name: String, val description: String?, val required: Boolean?) + +@Serializable +data class PromptInfoJSON( + val name: String, + val description: String?, + val arguments: List?, +) + +@Serializable +data class ResourceInfoJSON( + val uri: String, + val name: String?, + val description: String?, + val mimeType: String?, +) + +@Serializable +data class McpCapabilitiesJSON( + val serverName: String, + val serverVersion: String, + val tools: List, + val prompts: List, + val resources: List, +) diff --git a/codyze-console/src/main/resources/application.conf b/codyze-console/src/main/resources/application.conf index a409534d290..95bb782d1c6 100644 --- a/codyze-console/src/main/resources/application.conf +++ b/codyze-console/src/main/resources/application.conf @@ -22,8 +22,8 @@ llm { } vLLM { - baseUrl = "http://172.31.6.131:8001" - model = "cpatonn/Qwen3-Next-80B-A3B-Instruct-AWQ-4bit" + baseUrl = "http://172.31.6.131:8000" + model = "Qwen/Qwen3.5-122B-A10B-FP8" } } diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte index 198dbadc001..a612bb748ff 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte @@ -5,11 +5,13 @@ import DfgFlowWidget from './widgets/DfgFlowWidget.svelte'; import CodePreview from './CodePreview.svelte'; import ApiService from '$lib/services/apiService'; - import type { NodeJSON, AnalysisResultJSON, TranslationUnitJSON, ChatMessage } from '$lib/types'; + import type { NodeJSON, AnalysisResultJSON, TranslationUnitJSON, ChatMessage, McpCapabilities } from '$lib/types'; // State for code preview split-view let selectedNode = $state(null); let showCodePreview = $derived(selectedNode !== null); + let showRightPanel = $derived(showCodePreview); + function handleNodeClick(node: NodeJSON) { selectedNode = node; @@ -19,7 +21,6 @@ selectedNode = null; } - // Helper to find the TranslationUnit for a node function findTranslationUnit(node: NodeJSON): TranslationUnitJSON | null { if (!analysisResult) return null; @@ -64,9 +65,12 @@ streamingContent: string; isThinking: boolean; analysisResult?: AnalysisResultJSON | null; + mcpCapabilities?: McpCapabilities | null; onSendMessage: () => void; onReset: () => void; onMessageChange: (message: string) => void; + onPromptSelect?: (name: string, args: Record) => void; + onOpenMcp?: () => void; } let { @@ -76,9 +80,12 @@ streamingContent, isThinking, analysisResult, + mcpCapabilities, onSendMessage, onReset, - onMessageChange + onMessageChange, + onPromptSelect, + onOpenMcp }: Props = $props(); // Only show content when there's actual visible text @@ -132,22 +139,22 @@
-
- -
+
+ +
@@ -261,7 +268,24 @@ onValueChange={onMessageChange} placeholder="Ask me about your codebase..." disabled={isLoading} + prompts={mcpCapabilities?.prompts} + onPromptSelect={onPromptSelect} /> + {#if mcpCapabilities && onOpenMcp} +
+ +
+ {/if}
@@ -280,6 +304,7 @@ {/if}
+ + \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/MarkdownRenderer.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/MarkdownRenderer.svelte index 89df66e0713..011440b77b3 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/MarkdownRenderer.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/MarkdownRenderer.svelte @@ -8,23 +8,18 @@ let { content }: Props = $props(); - // Configure marked options for better rendering - marked.setOptions({ - gfm: true, // GitHub Flavored Markdown - breaks: true // Convert \n to
- }); - - // Custom renderer for code blocks with syntax highlighting const renderer = new marked.Renderer(); - - renderer.code = function({ text, lang }: Tokens.Code): string { + renderer.code = function ({ text, lang }: Tokens.Code): string { const language = lang || 'plaintext'; return `
${text}
`; }; - marked.use({ renderer }); + marked.use({ + gfm: true, // GitHub Flavored Markdown + breaks: true, // Convert \n to
+ renderer + }); - // Parse markdown content let html = $derived(marked.parse(content) as string); diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/McpCapabilitiesModal.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/McpCapabilitiesModal.svelte index ce1a73a9db9..53d34488499 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/McpCapabilitiesModal.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/McpCapabilitiesModal.svelte @@ -17,7 +17,8 @@ { id: 'resources', label: 'Resources', count: capabilities.resources.length } ]); - function getParamEntries(schema: any): { name: string; type: string; required: boolean }[] { + /** Extracts the properties of the MCP tool definitions */ + function getMcpToolProps(schema: any): { name: string; type: string; required: boolean }[] { if (!schema?.properties) return []; const required: string[] = schema.required ?? []; return Object.entries(schema.properties).map(([name, def]: [string, any]) => ({ @@ -27,10 +28,12 @@ })); } - function isLong(text: string | undefined): boolean { + /** Descriptions longer than this are truncated in the collapsed state */ + function shouldTruncateDescription(text: string | undefined): boolean { return !!text && text.length > 120; } + /** Only close when clicking the backdrop itself, not its children */ function handleBackdropClick(e: MouseEvent) { if (e.target === e.currentTarget) onClose(); } @@ -49,9 +52,9 @@ onkeydown={handleKeydown} > -
+
-
+
@@ -77,7 +80,7 @@
-
+
(activeTab = id)} />
@@ -89,13 +92,13 @@ {:else}
    {#each capabilities.tools as tool} - {@const params = getParamEntries(tool.inputSchema)} + {@const params = getMcpToolProps(tool.inputSchema)}
  • @@ -110,7 +113,7 @@ {/if}
{#if tool.description} - {#if isLong(tool.description)} + {#if shouldTruncateDescription(tool.description)}

{tool.description}

diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/MessageInput.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/MessageInput.svelte index 62fd5c5f609..a3cd367d10d 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/MessageInput.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/MessageInput.svelte @@ -7,6 +7,7 @@ onValueChange: (value: string) => void; placeholder?: string; disabled?: boolean; + /** MCP prompts available for slash-command completion (e.g. typing "/suggest") */ prompts?: McpPromptInfo[]; onPromptSelect?: (name: string, args: Record) => void; } @@ -22,39 +23,48 @@ }: Props = $props(); let textareaElement: HTMLTextAreaElement; + /** Index of the currently highlighted prompt in the slash-command picker */ let pickerIndex = $state(0); - const slashQuery = $derived(() => { + /** + * Returns the text after the leading slash when the input matches "/word" exactly. + */ + const slashQuery = $derived.by(() => { if (!prompts || prompts.length === 0) return null; const match = value.match(/^\/(\S*)$/); return match ? match[1] : null; }); - const filteredPrompts = $derived(() => { - const q = slashQuery(); - if (q === null) return []; - const lower = q.toLowerCase(); + /** Prompts whose names contain the current slash query (case-insensitive) */ + const filteredPrompts = $derived.by(() => { + if (slashQuery === null) return []; + const lower = slashQuery.toLowerCase(); return prompts!.filter((p) => p.name.toLowerCase().includes(lower)); }); - const showPicker = $derived(filteredPrompts().length > 0); + const showPicker = $derived(filteredPrompts.length > 0); + function resetTextareaHeight() { + if (textareaElement) textareaElement.style.height = 'auto'; + } + + // Handles keyboard navigation for the slash-command prompt picker, + // and sends the message on Enter. function handleKeyDown(e: KeyboardEvent) { if (showPicker) { - const fp = filteredPrompts(); if (e.key === 'ArrowDown') { e.preventDefault(); - pickerIndex = (pickerIndex + 1) % fp.length; + pickerIndex = (pickerIndex + 1) % filteredPrompts.length; return; } if (e.key === 'ArrowUp') { e.preventDefault(); - pickerIndex = (pickerIndex - 1 + fp.length) % fp.length; + pickerIndex = (pickerIndex - 1 + filteredPrompts.length) % filteredPrompts.length; return; } if (e.key === 'Enter') { e.preventDefault(); - selectPrompt(fp[pickerIndex]); + selectPrompt(filteredPrompts[pickerIndex]); return; } if (e.key === 'Escape') { @@ -73,39 +83,34 @@ function handleInput(e: Event) { const target = e.target as HTMLTextAreaElement; onValueChange(target.value); + // Auto-grow up to 120px, then scroll target.style.height = 'auto'; target.style.height = Math.min(target.scrollHeight, 120) + 'px'; - pickerIndex = 0; + pickerIndex = 0; // reset picker selection on every keystroke } function selectPrompt(prompt: McpPromptInfo) { onValueChange(''); - if (textareaElement) textareaElement.style.height = 'auto'; - if (onPromptSelect) { - onPromptSelect(prompt.name, {}); - } + resetTextareaHeight(); + onPromptSelect?.(prompt.name, {}); } function handleSend() { if (value.trim() && !disabled) { onSend(); - if (textareaElement) { - textareaElement.style.height = 'auto'; - } + resetTextareaHeight(); } } + // When the parent clears the value (e.g. after sending), reset textarea height too $effect(() => { - if (value === '' && textareaElement) { - textareaElement.style.height = 'auto'; - } + if (value === '') resetTextareaHeight(); });
- + {#if showPicker} - {@const fp = filteredPrompts()}
@@ -113,7 +118,7 @@

Available prompts

    - {#each fp as prompt, i} + {#each filteredPrompts as prompt, i}
-
+
\ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/WelcomeScreen.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/WelcomeScreen.svelte index d1d0ce27b93..98a9a316117 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/WelcomeScreen.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/WelcomeScreen.svelte @@ -5,11 +5,11 @@ interface Props { onWelcomeMessage: (message: string) => void; mcpCapabilities?: McpCapabilities | null; - onOpenMcp?: () => void; + onOpenMcpModal?: () => void; onPromptSelect?: (name: string, args: Record) => void; } - let { onWelcomeMessage, mcpCapabilities, onOpenMcp, onPromptSelect }: Props = $props(); + let { onWelcomeMessage, mcpCapabilities, onOpenMcpModal, onPromptSelect }: Props = $props(); let messageInput = $state(''); @@ -88,11 +88,11 @@ prompts={mcpCapabilities?.prompts} onPromptSelect={onPromptSelect} /> - {#if mcpCapabilities && onOpenMcp} + {#if mcpCapabilities && onOpenMcpModal}
diff --git a/codyze-console/src/main/webapp/vite.config.ts b/codyze-console/src/main/webapp/vite.config.ts index 65a884eadc8..2eaee2d76e4 100644 --- a/codyze-console/src/main/webapp/vite.config.ts +++ b/codyze-console/src/main/webapp/vite.config.ts @@ -6,18 +6,7 @@ export default defineConfig({ plugins: [tailwindcss(), sveltekit()], server: { proxy: { - '/api': { - target: 'http://localhost:8080', - changeOrigin: true, - timeout: 0, - proxyTimeout: 0, - configure: (proxy) => { - proxy.on('proxyReq', (proxyReq) => { - // Ensure connection stays alive for SSE - proxyReq.setHeader('Connection', 'keep-alive'); - }); - } - } + '/api': 'http://localhost:8080' } } }); diff --git a/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/JsonSchemaGeneratorTest.kt b/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/JsonSchemaGeneratorTest.kt index b145fa42ac4..f5690852948 100644 --- a/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/JsonSchemaGeneratorTest.kt +++ b/cpg-mcp/src/test/kotlin/de/fraunhofer/aisec/cpg/mcp/JsonSchemaGeneratorTest.kt @@ -45,13 +45,13 @@ class JsonSchemaGeneratorTest { ToolSchema( properties = buildJsonObject { - putJsonObject("items") { + putJsonObject("assignments") { put("type", "array") - put("description", "List of items") + put("description", "List of concept assignments to perform") putJsonObject("items") { put("type", "object") putJsonObject("properties") { - putJsonObject("entries") { + putJsonObject("arguments") { put("type", "array") putJsonObject("items") { put("type", "object") @@ -72,16 +72,19 @@ class JsonSchemaGeneratorTest { put("type", "string") put("description", "Required id") } - putJsonObject("tag") { + putJsonObject("securityImpact") { put("type", "string") - put("description", "Optional tag") + put( + "description", + "A description if this concept could have security implications (optional)", + ) } } putJsonArray("required") { add("id") } } } }, - required = listOf("items"), + required = listOf("assignments"), ) val actual = TestPayload::class.toSchema() @@ -94,11 +97,14 @@ class JsonSchemaGeneratorTest { private data class TestPair(@Description("The key") val key: String, val value: String) @Serializable -private data class TestItem( +private data class TestAssignment( @Description("Required id") val id: String, - @Description("Optional tag") val tag: String? = null, - val entries: List? = null, + @Description("A description if this concept could have security implications (optional)") + val securityImpact: String? = null, + val arguments: List? = null, ) @Serializable -private data class TestPayload(@Description("List of items") val items: List) +private data class TestPayload( + @Description("List of concept assignments to perform") val assignments: List +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 712e4ed5b26..ff6f2fa5eeb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,6 +66,9 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx ktor-server-test-host = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" } +ktor-server-sse = { module = "io.ktor:ktor-server-sse", version.ref = "ktor" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } mcp = {module = "io.modelcontextprotocol:kotlin-sdk", version = "0.8.4"} mcp-client = {module = "io.modelcontextprotocol:kotlin-sdk-client", version = "0.8.4"} @@ -95,7 +98,8 @@ dex2jar = {module = "de.femtopedia.dex2jar:dex2jar", version="2.4.22" } log4j = ["log4j-impl", "log4j-core"] neo4j = ["neo4j-ogm-core", "neo4j-ogm-bolt-driver"] sootup = ["sootup-core", "sootup-java-core", "sootup-java-sourcecode", "sootup-java-bytecode", "sootup-jimple", "sootup-apk"] -ktor = ["ktor-server-core", "ktor-server-netty", "ktor-server-cors","ktor-server-content-negotiation", "ktor-serialization-kotlinx-json"] +ktor = ["ktor-server-core", "ktor-server-netty", "ktor-server-cors","ktor-server-content-negotiation", "ktor-serialization-kotlinx-json", "ktor-server-sse"] +ktor-client = ["ktor-client-core", "ktor-client-cio", "ktor-client-content-negotiation"] [plugins] kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"} From 3ed268a92505a6c8f97306a487fe86ce910103b7 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 9 Mar 2026 17:52:37 +0100 Subject: [PATCH 91/93] Make CodeViewer, Filetree and NodeTable components reusable for ChatInterface --- .../components/ai-agent/ChatInterface.svelte | 400 ++++++++---------- .../components/ai-agent/CodePreview.svelte | 212 ---------- .../components/ai-agent/MessageInput.svelte | 20 +- .../lib/components/analysis/CodeViewer.svelte | 239 ++++++----- .../lib/components/analysis/FileTree.svelte | 255 ++++++----- .../lib/components/analysis/NodeTable.svelte | 94 ++-- .../navigation/TabNavigation.svelte | 40 +- .../components/[componentName]/+layout.svelte | 221 +--------- .../translation-unit/[unitId]/+page.svelte | 147 +------ .../mcp/mcpserver/tools/utils/Serializable.kt | 3 + .../cpg/mcp/mcpserver/tools/utils/Util.kt | 3 + 11 files changed, 558 insertions(+), 1076 deletions(-) delete mode 100644 codyze-console/src/main/webapp/src/lib/components/ai-agent/CodePreview.svelte diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte index cee6bd19a28..47e51b25272 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/ChatInterface.svelte @@ -3,54 +3,86 @@ import MessageInput from './MessageInput.svelte'; import CodeItemList, { isCodeItemContent } from './widgets/CodeItemList.svelte'; import DfgFlowWidget from './widgets/DfgFlowWidget.svelte'; - import CodePreview from './CodePreview.svelte'; - import type { NodeJSON, AnalysisResultJSON, TranslationUnitJSON, ChatMessage, McpCapabilities } from '$lib/types'; + import { CodeViewer, FileTree } from '$lib/components/analysis'; + import type { NodeJSON, AnalysisResultJSON, TranslationUnitJSON, ChatMessage, McpCapabilities, ComponentJSON } from '$lib/types'; - // State for code preview split-view let selectedNode = $state(null); - let showCodePreview = $derived(selectedNode !== null); + let selectedTranslationUnit = $state(null); + let selectedComponentName = $state(null); + let overlayNodes = $state([]); + let astNodes = $state([]); + let showCodePanel = $derived(selectedTranslationUnit !== null); + let fileTreeCollapsed = $state(false); + let nodesPanelCollapsed = $state(false); - function handleNodeClick(node: NodeJSON) { - selectedNode = node; - } - - function closeCodePreview() { - selectedNode = null; + async function loadNodes(componentName: string, tuId: string) { + const [overlay, ast] = await Promise.all([ + fetch(`/api/component/${componentName}/translation-unit/${tuId}/overlay-nodes`).then(r => r.json()).catch(() => []), + fetch(`/api/component/${componentName}/translation-unit/${tuId}/ast-nodes`).then(r => r.json()).catch(() => []), + ]); + overlayNodes = overlay; + astNodes = ast; } function findTranslationUnit(node: NodeJSON): TranslationUnitJSON | null { if (!analysisResult) return null; - - // Try to find by translationUnitId if (node.translationUnitId) { for (const component of analysisResult.components) { const tu = component.translationUnits.find((tu) => tu.id === node.translationUnitId); if (tu) return tu; } } - - // Fallback: Try to find by fileName if (node.fileName) { for (const component of analysisResult.components) { const tu = component.translationUnits.find((tu) => tu.name.includes(node.fileName!)); if (tu) return tu; } } - + if ((node as any).componentName) { + const component = analysisResult.components.find((c) => c.name === (node as any).componentName); + if (component?.translationUnits.length) return component.translationUnits[0]; + } return null; } - // Returns the TranslationUnit for a node, or a minimal fallback if not found - function getTranslationUnitForNode(node: NodeJSON): TranslationUnitJSON { - return findTranslationUnit(node) ?? { - id: node.translationUnitId || 'fallback-tu', - name: node.fileName || 'unknown', - path: `file:///${node.fileName || 'unknown'}`, - code: node.code || '// Code not available', - findings: [] - }; + function handleNodeClick(node: NodeJSON) { + const tu = findTranslationUnit(node); + if (!tu) return; + selectedNode = node; + selectedTranslationUnit = tu; + const comp = findComponentForTu(tu.id); + selectedComponentName = comp?.name ?? null; + if (comp) loadNodes(comp.name, tu.id); + } + + function handleFileSelect(unit: TranslationUnitJSON) { + selectedTranslationUnit = unit; + selectedNode = null; + const comp = findComponentForTu(unit.id); + selectedComponentName = comp?.name ?? null; + if (comp) loadNodes(comp.name, unit.id); } + function closeCodePanel() { + selectedNode = null; + selectedTranslationUnit = null; + selectedComponentName = null; + overlayNodes = []; + astNodes = []; + } + + function findComponentForTu(tuId: string): ComponentJSON | null { + if (!analysisResult) return null; + return analysisResult.components.find((c) => + c.translationUnits.some((tu) => tu.id === tuId) + ) ?? null; + } + + const selectedComponent = $derived((): ComponentJSON | null => { + if (!selectedTranslationUnit || !analysisResult) return null; + return findComponentForTu(selectedTranslationUnit.id); + }); + interface Props { messages: ChatMessage[]; currentMessage: string; @@ -81,17 +113,14 @@ onOpenMcpModal }: Props = $props(); - // Only show content when there's actual visible text let displayContent = $derived(streamingContent.trim().length > 0 ? streamingContent : ''); - // Auto-scroll to bottom let messagesContainer: HTMLDivElement; let shouldAutoScroll = $state(true); - // Check if user is near the bottom of the scroll container function isNearBottom(): boolean { if (!messagesContainer) return true; - const threshold = 150; // pixels from bottom + const threshold = 150; const position = messagesContainer.scrollTop + messagesContainer.clientHeight; return messagesContainer.scrollHeight - position < threshold; } @@ -106,7 +135,6 @@ } } - // Auto-scroll when messages change or streaming content updates $effect(() => { messages.length; streamingContent; @@ -115,7 +143,6 @@ } }); - // Per-message toggle state for the "Thought process" reasoning panel let expandedReasoning = $state>(new Set()); function toggleReasoning(id: string) { @@ -130,217 +157,156 @@
- -
- -
- -
- - -
-
- {#each messages as message} - {#if message.role === 'user'} - -
-
-
-
{message.content}
+ +
+ +
+
+ {#each messages as message} + {#if message.role === 'user'} +
+
+
+
{message.content}
+
-
- {:else} - -
- - {#if message.reasoning} -
- - {#if expandedReasoning.has(message.id)} -
-

- {message.reasoning} -

+ + + + + + + Thought process + + {#if expandedReasoning.has(message.id)} +
+

{message.reasoning}

+
+ {/if} +
+ {/if} + {#if message.contentType === 'tool-result' && message.toolResult} + {#if message.toolResult.toolName === 'cpg_dfg_backward'} + + {:else if isCodeItemContent(message.toolResult.content)} + + {:else} +
+
+ {#if message.toolResult.toolName} + {message.toolResult.toolName} + {/if} + {#if message.toolResult.isError} + Error + {/if} +
+
{typeof message.toolResult.content === 'string' ? message.toolResult.content : JSON.stringify(message.toolResult.content, null, 2)}
{/if} -
- {/if} - {#if message.contentType === 'tool-result' && message.toolResult} - {#if message.toolResult.toolName === 'cpg_dfg_backward'} - - {:else if isCodeItemContent(message.toolResult.content)} - - {:else} - -
-
- {#if message.toolResult.toolName} - {message.toolResult.toolName} - {/if} - {#if message.toolResult.isError} - Error - {/if} -
-
{typeof message.toolResult.content === 'string' ? message.toolResult.content : JSON.stringify(message.toolResult.content, null, 2)}
+ {:else if message.content} +
+
{/if} - {:else if message.content} - +
+ {/if} + {/each} + + {#if isLoading || displayContent} +
+ {#if displayContent}
- + +
+ {:else} +
+
+
+
+
+
{/if}
{/if} - {/each} +
+
- {#if isLoading || displayContent} - -
- {#if displayContent} -
- -
- {:else} - -
-
-
-
-
-
-
- {/if} -
- {/if} + +
+
+ + {#if mcpCapabilities && onOpenMcpModal} +
+ +
+ {/if} +
- -
-
- - {#if mcpCapabilities && onOpenMcpModal} -
- -
+ + {#if showCodePanel && selectedTranslationUnit} +
+ + {#if selectedComponent()} + {/if} -
-
-
- - {#if showCodePreview && selectedNode} -
- +
{/if}
- \ No newline at end of file diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/CodePreview.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/CodePreview.svelte deleted file mode 100644 index 1599ae5d263..00000000000 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/CodePreview.svelte +++ /dev/null @@ -1,212 +0,0 @@ - - -
- -
-
- - - -
-
{node.name}
-
- {fileName}:{node.startLine} -
-
-
- -
- - -
- -
- - - -
- - diff --git a/codyze-console/src/main/webapp/src/lib/components/ai-agent/MessageInput.svelte b/codyze-console/src/main/webapp/src/lib/components/ai-agent/MessageInput.svelte index a3cd367d10d..2c3dac7a313 100644 --- a/codyze-console/src/main/webapp/src/lib/components/ai-agent/MessageInput.svelte +++ b/codyze-console/src/main/webapp/src/lib/components/ai-agent/MessageInput.svelte @@ -10,6 +10,7 @@ /** MCP prompts available for slash-command completion (e.g. typing "/suggest") */ prompts?: McpPromptInfo[]; onPromptSelect?: (name: string, args: Record) => void; + onNewChat?: () => void; } let { @@ -19,7 +20,8 @@ placeholder = 'Ask me anything about your codebase...', disabled = false, prompts, - onPromptSelect + onPromptSelect, + onNewChat }: Props = $props(); let textareaElement: HTMLTextAreaElement; @@ -138,8 +140,22 @@ {/if}
+ {#if onNewChat} + +
+ {/if}