From 18840d5d77cceebd96695e8a834e22defa912311 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Fri, 28 Jun 2024 16:15:57 +0200 Subject: [PATCH 1/2] Use Kubescape for AI cluster scan --- hack/assistant-setup/file-search.ts | 3 + hack/assistant-setup/index.ts | 2 +- hack/assistant-setup/tools.ts | 93 +++++++++++++ internal/source/ai-brain/assistant.go | 121 ++++++++++------- internal/source/ai-brain/config.go | 12 +- internal/source/ai-brain/kubescape_tools.go | 123 ++++++++++++++++++ internal/source/ai-brain/response.go | 14 +- internal/source/ai-brain/response_test.go | 18 ++- .../teams-tools-no-report.golden.json | 8 ++ internal/source/ai-brain/tool_calls.go | 4 + 10 files changed, 338 insertions(+), 60 deletions(-) create mode 100644 internal/source/ai-brain/kubescape_tools.go create mode 100644 internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/teams-tools-no-report.golden.json diff --git a/hack/assistant-setup/file-search.ts b/hack/assistant-setup/file-search.ts index 5d2c29e..796c307 100644 --- a/hack/assistant-setup/file-search.ts +++ b/hack/assistant-setup/file-search.ts @@ -22,6 +22,9 @@ export async function setupFileSearch(client: OpenAI): Promise { const vectorStore = await client.beta.vectorStores.create({ name: vectorStoreName, }); + console.log( + `Created vector store '${vectorStore.name}' (ID: ${vectorStore.id})`, + ); console.log( "Uploading files to vector store and waiting for the file batch processing to complete. This might take a few minutes...", diff --git a/hack/assistant-setup/index.ts b/hack/assistant-setup/index.ts index be9bfc5..94d8786 100644 --- a/hack/assistant-setup/index.ts +++ b/hack/assistant-setup/index.ts @@ -47,7 +47,7 @@ const instructions = dedent` async function main() { let cfg: Config = { - projectID: "", + projectID: undefined, assistantID: "", }; const assistantEnv = process.env["ASSISTANT_ENV"]; diff --git a/hack/assistant-setup/tools.ts b/hack/assistant-setup/tools.ts index 1c1ff38..d892b28 100644 --- a/hack/assistant-setup/tools.ts +++ b/hack/assistant-setup/tools.ts @@ -237,5 +237,98 @@ export function setupTools(): Array { }, }, }, + { + type: "function", + function: { + name: "kubescapeScanCluster", + description: dedent` + It serves as an all-in-one tool for vulnerability and misconfiguration scanning for the whole Kubernetes cluster. + Kubescape includes misconfiguration and vulnerability scanning as well as risk analysis and security compliance indicators. + All results are presented in context and users get many cues on what to do based on scan results. + It saves Kubernetes users and admins precious time, effort, and resources. + `, + }, + }, + { + type: "function", + function: { + name: "kubescapeScanWorkload", + description: dedent` + Allows you to comprehensively report on the security posture of individual workloads running in a Kubernetes cluster. + This includes both misconfiguration and image vulnerability scanning. + This scan results in information that gives a 360-degree assessment of your workload's security posture. + + Usage: + # Scan a workload + kubescape scan workload {kind}/{name} + # Scan a workload in a specific namespace + kubescape scan workload {kind}/{name} --namespace {namespace} + `, + parameters: { + type: "object", + properties: { + namespace: { + type: "string", + description: "Kubernetes namespace, e.g. kube-system", + }, + resource_kind: { + type: "string", + description: + "Kubernetes workload kind, e.g. Deployment or StatefulSet.", + }, + resource_name: { + type: "string", + description: "Kubernetes workload name, e.g. botkube-api-server.", + }, + }, + required: ["resource_kind", "resource_name"], + }, + }, + }, + { + type: "function", + function: { + name: "kubescapeScanImage", + description: dedent` + Scan an image for vulnerabilities. + + Usage: + kubescape scan image "nginx" + kubescape scan image "nginx:latest" + `, + parameters: { + type: "object", + properties: { + image: { + type: "string", + description: "Image name with tag, e.g. nginx:latest", + }, + }, + required: ["image"], + }, + }, + }, + { + type: "function", + function: { + name: "kubescapeScanControl", + description: dedent` + Allows you to get details about a given Kubescape issue based on ID like "C-0188" or "C-0007". + + Usage: + kubescape scan control {control ID} + `, + parameters: { + type: "object", + properties: { + control: { + type: "string", + description: "Control ID, e.g. C-0188.", + }, + }, + required: ["control"], + }, + }, + }, ]; } diff --git a/internal/source/ai-brain/assistant.go b/internal/source/ai-brain/assistant.go index b9f7853..f609a84 100644 --- a/internal/source/ai-brain/assistant.go +++ b/internal/source/ai-brain/assistant.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "github.com/MakeNowJust/heredoc" + "github.com/kubeshop/botkube/pkg/ptr" "github.com/kubeshop/botkube-cloud-plugins/internal/otelx" @@ -28,52 +30,61 @@ import ( ) const ( - cacheTTL = 8 * time.Hour - openAIPollInterval = 2 * time.Second - maxToolExecutionRetries = 3 - quotaExceededErrCode = "quota_exceeded" - tracerName = "source.aibrain" - serviceName = "botkube-plugins-source-ai-brain" - clusterScanSubcommandName = "scan" - - clusterScanPrompt = ` -Scan the Kubernetes cluster for critical issues that could significantly impact the cluster's health, stability, or security. -Focus on problems that may not be immediately apparent through events or standard monitoring. - -Provide a concise overview of the scan results, including the total number of -critical issues found. If there were no issues found for a specific check, do -not include that section in the report. List the Kubernetes objects directly -affected by the issue. Make sure that your checks are relevant to the current -state of the cluster, do not include resources that no longer exist. - -Summary section needs to be at the top of the report, followed by specific checks. - -Specific Checks: - -Pod Health: -Identify pods in a crash-loop backoff state with a high restart count. -Identify pods that have been OOMKilled (Out of Memory Killed) multiple times. -Look for pods stuck in a pending state for an extended period. -Resource Utilization: -Identify nodes or pods with critically high CPU or memory usage (e.g., above 90% of limits). -Check for critical resource starvation issues affecting multiple pods or namespaces. -Configuration: -Look for pods running with very insecure capabilities (e.g., ALL, NET_RAW, SYS_ADMIN). -Identify pods using deprecated or insecure container images. -Check for misconfigured network policies that could expose sensitive services. -Networking: -Identify pods or services experiencing significant network latency or packet loss. -Check for network partitions or connectivity issues between critical components. - -Additional Guidance for the LLM Agent: - -Prioritize issues that pose the most immediate threat to the cluster's stability, performance, or security. -Filter out informational or low-severity issues that are unlikely to cause major problems. -Be as specific as possible in the descriptions. Do not exceed 2000 characters in your response. -` + cacheTTL = 8 * time.Hour + openAIPollInterval = 2 * time.Second + maxToolExecutionRetries = 3 + quotaExceededErrCode = "quota_exceeded" + serviceName = "botkube-plugins-source-ai-brain" + temperature float32 = 0.1 + msgSplitPattern = "\n\n---\n\n" + clusterScanSubcommandName = "scan" + multipleMessagesDelay = 500 * time.Millisecond ) -var temperature float32 = 0.1 +var ( + clusterScanPrompt = heredoc.Doc(` + Scan the Kubernetes cluster for critical issues that could significantly impact the cluster's health, stability, or security. + Focus on problems that may not be immediately apparent through events or standard monitoring. + Use Kubescape and kubectl tools to scan the cluster, and then aggregate the results based on the instructions. + Prioritize Kubescape scan results over the kubectl tools results. Include links for Kubescape controls which you got them from Kubescape scan results. + + Provide a concise overview of the scan results, including the total number of issues found. + If there were no issues found for a specific check, do not include that section in the report. + List the Kubernetes objects directly affected by the issue. + Make sure that your checks are relevant to the current state of the cluster, do not include resources that no longer exist. + + Summary section needs to be at the top of the report, followed by specific checks. + Summary outlines what are the issues and how many of them were found, and one line sentence about the overall cluster state based on the results. + Use emojis for the severity of the issues in the summary (critical/high/medium/low), and also for the headlines of the checks to distinguish them. + Use a separator "\n\n---\n\n" to split the message into TWO logical sections, no more. + + Specific checks: + + Pod Health: + Identify pods in a crash-loop backoff state with a high restart count. + Identify pods that have been OOMKilled (Out of Memory Killed) multiple times. + Look for pods stuck in a pending state for an extended period. + Resource Utilization: + Identify nodes or pods with critically high CPU or memory usage. By critically high we mean over 90% or more. + Check for critical resource starvation issues affecting multiple pods or namespaces. + Configuration: + Look for pods running with very insecure capabilities (e.g., ALL, NET_RAW, SYS_ADMIN). + Identify pods using deprecated or insecure container images. + Check for misconfigured network policies that could expose sensitive services. + Networking: + Identify pods or services experiencing significant network latency or packet loss. + Check for network partitions or connectivity issues between critical components. + Security + Under this section, include Security posture from Kubescape scan. + + Additional Guidance for the LLM Agent: + + Prioritize issues that pose the most immediate threat to the cluster's stability, performance, or security. + Skip the check output if there are no issues found for a given check. Filter out informational issues. + Be as specific as possible in the descriptions. Do not exceed 3000 characters in your response. + Don't show kubescape commands. + At the end of the message, add "Feel free to ask me to provide additional details, or help on how to resolve found issues!", without a separator, in any form you like.`) +) type tool func(ctx context.Context, args []byte, p *Payload) (string, error) @@ -105,6 +116,7 @@ func newAssistant(cfg *Config, log logrus.FieldLogger, out chan source.Event, ku tracer := otel.Tracer(serviceName) kcRunner := NewKubectlRunner(kubeConfigPath, tracer) + ksRunner := NewKubescapeRunner(kubeConfigPath, tracer) bkRunner, err := NewBotkubeRunner(tracer) if err != nil { return nil, fmt.Errorf("while creating Botkube runner: %w", err) @@ -133,6 +145,10 @@ func newAssistant(cfg *Config, log logrus.FieldLogger, out chan source.Event, ku "kubectlLogs": kcRunner.Logs, "botkubeGetStartupAgentConfiguration": bkRunner.GetStartupAgentConfiguration, "botkubeGetAgentStatus": bkRunner.GetAgentStatus, + "kubescapeScanCluster": ksRunner.ScanCluster, + "kubescapeScanWorkload": ksRunner.ScanWorkload, + "kubescapeScanControl": ksRunner.ScanControl, + "kubescapeScanImage": ksRunner.ScanImage, }, vectorStoreIDForThread: cfg.VectorStoreIDForThread, }, nil @@ -226,7 +242,7 @@ func (i *assistant) handleThread(ctx context.Context, p *Payload) (err error) { }) run, err := i.openaiClient.CreateRun(ctx, threadID, openai.RunRequest{ AssistantID: i.assistID, - Temperature: &temperature, + Temperature: ptr.FromType(temperature), }) if err != nil { return fmt.Errorf("while creating a thread run: %w", err) @@ -359,8 +375,19 @@ func (i *assistant) handleStatusCompleted(ctx context.Context, run openai.Run, p textValue := i.trimCitationsIfPresent(i.log, c.Text) - i.out <- source.Event{ - Message: msgAIAnswer(run, p, textValue, toolCalls), + msgs := strings.Split(textValue, msgSplitPattern) + isMultiMessage := len(msgs) > 1 + for j, msg := range msgs { + isLastMessage := j == len(msgs)-1 + i.out <- source.Event{ + Message: msgAIAnswer(run, p, msg, toolCalls, isLastMessage), + } + + if isMultiMessage { + // Ugly workaround to force ordering of messages in the same thread + // Probably PubSub related? + time.Sleep(multipleMessagesDelay) + } } } diff --git a/internal/source/ai-brain/config.go b/internal/source/ai-brain/config.go index 3c5457c..40f40e9 100644 --- a/internal/source/ai-brain/config.go +++ b/internal/source/ai-brain/config.go @@ -66,10 +66,16 @@ func binaryDependencies() map[string]api.Dependency { "darwin/amd64": fmt.Sprintf("https://dl.k8s.io/release/%s/bin/darwin/amd64/kubectl", kubectlVersion), "darwin/arm64": fmt.Sprintf("https://dl.k8s.io/release/%s/bin/darwin/arm64/kubectl", kubectlVersion), "linux/amd64": fmt.Sprintf("https://dl.k8s.io/release/%s/bin/linux/amd64/kubectl", kubectlVersion), - "linux/s390x": fmt.Sprintf("https://dl.k8s.io/release/%s/bin/linux/s390x/kubectl", kubectlVersion), - "linux/ppc64le": fmt.Sprintf("https://dl.k8s.io/release/%s/bin/linux/ppc64le/kubectl", kubectlVersion), "linux/arm64": fmt.Sprintf("https://dl.k8s.io/release/%s/bin/linux/arm64/kubectl", kubectlVersion), - "linux/386": fmt.Sprintf("https://dl.k8s.io/release/%s/bin/linux/386/kubectl", kubectlVersion), + }, + }, + kubescapeBinaryName: { + URLs: map[string]string{ + "windows/amd64": fmt.Sprintf("https://github.com/kubescape/kubescape/releases/download/%s/kubescape-arm64-macos-latest", kubescapeVersion), + "darwin/amd64": fmt.Sprintf("https://github.com/kubescape/kubescape/releases/download/%s/kubescape-macos-latest", kubescapeVersion), + "darwin/arm64": fmt.Sprintf("https://github.com/kubescape/kubescape/releases/download/%s/kubescape-macos-latest", kubescapeVersion), + "linux/amd64": fmt.Sprintf("https://github.com/kubescape/kubescape/releases/download/%s/kubescape-ubuntu-latest", kubescapeVersion), + "linux/arm64": fmt.Sprintf("https://github.com/kubescape/kubescape/releases/download/%s/kubescape-arm64-ubuntu-latest", kubescapeVersion), }, }, } diff --git a/internal/source/ai-brain/kubescape_tools.go b/internal/source/ai-brain/kubescape_tools.go new file mode 100644 index 0000000..447d537 --- /dev/null +++ b/internal/source/ai-brain/kubescape_tools.go @@ -0,0 +1,123 @@ +package aibrain + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/gookit/color" + "github.com/kubeshop/botkube/pkg/plugin" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +const ( + kubescapeBinaryName = "kubescape" + kubescapeVersion = "v3.0.11" +) + +type KubescapeRunner struct { + tracer trace.Tracer + kubeconfigPath string +} + +func NewKubescapeRunner(kubeconfigPath string, tracer trace.Tracer) *KubescapeRunner { + return &KubescapeRunner{ + kubeconfigPath: kubeconfigPath, + tracer: tracer, + } +} + +func (k *KubescapeRunner) ScanCluster(ctx context.Context, _ []byte, _ *Payload) (string, error) { + ctx, span := k.tracer.Start(ctx, "aibrain.KubescapeRunner.ScanCluster") + defer span.End() + + cmd := "scan --verbose" + return k.runKubescapeCommand(ctx, cmd) +} + +type scanWorkloadArgs struct { + ResourceKind string `json:"resource_kind"` + ResourceName string `json:"resource_name"` + Namespace string `json:"namespace,omitempty"` +} + +func (k *KubescapeRunner) ScanWorkload(ctx context.Context, rawArgs []byte, _ *Payload) (string, error) { + ctx, span := k.tracer.Start(ctx, "aibrain.KubescapeRunner.ScanWorkload") + defer span.End() + + span.SetAttributes(attribute.String("kubescapeRunner.args", string(rawArgs))) + + var args scanWorkloadArgs + if err := json.Unmarshal(rawArgs, &args); err != nil { + return "", fmt.Errorf("invalid arguments: %w", err) + } + + cmd := fmt.Sprintf("scan -v workload %s/%s", args.ResourceKind, args.ResourceName) + if args.Namespace != "" { + cmd += fmt.Sprintf(" --namespace %s", args.Namespace) + } + return k.runKubescapeCommand(ctx, cmd) +} + +type scanImageArgs struct { + Image string `json:"image"` +} + +func (k *KubescapeRunner) ScanImage(ctx context.Context, rawArgs []byte, _ *Payload) (string, error) { + ctx, span := k.tracer.Start(ctx, "aibrain.KubescapeRunner.ScanImage") + defer span.End() + + span.SetAttributes(attribute.String("kubescapeRunner.args", string(rawArgs))) + + var args scanImageArgs + if err := json.Unmarshal(rawArgs, &args); err != nil { + return "", fmt.Errorf("invalid arguments: %w", err) + } + + cmd := fmt.Sprintf("scan -v image %s", args.Image) + return k.runKubescapeCommand(ctx, cmd) +} + +type scanControlArgs struct { + Control string `json:"control"` +} + +func (k *KubescapeRunner) ScanControl(ctx context.Context, rawArgs []byte, _ *Payload) (string, error) { + ctx, span := k.tracer.Start(ctx, "aibrain.KubescapeRunner.ScanControl") + defer span.End() + + span.SetAttributes(attribute.String("kubescapeRunner.args", string(rawArgs))) + + var args scanControlArgs + if err := json.Unmarshal(rawArgs, &args); err != nil { + return "", fmt.Errorf("invalid arguments: %w", err) + } + + cmd := fmt.Sprintf("scan -v control %s", args.Control) + return k.runKubescapeCommand(ctx, cmd) +} + +func (k *KubescapeRunner) runKubescapeCommand(ctx context.Context, cmd string) (string, error) { + ctx, span := k.tracer.Start(ctx, "aibrain.KubectlRunner.runKubescapeCommand") + defer span.End() + + envs := map[string]string{ + kubeconfigEnvVarName: k.kubeconfigPath, + } + + cmd = fmt.Sprintf("%s %s", kubescapeBinaryName, cmd) + span.SetAttributes(attribute.String("kubectlRunner.cmd", cmd)) + + out, err := plugin.ExecuteCommand(ctx, cmd, plugin.ExecuteCommandEnvs(envs)) + + if out.ExitCode != 0 { + return fmt.Sprintf("kubectl command failed: %s", color.ClearCode(out.CombinedOutput())), nil + } + + if err != nil { + return "", err + } + + return color.ClearCode(out.CombinedOutput()), nil +} diff --git a/internal/source/ai-brain/response.go b/internal/source/ai-brain/response.go index d81b9aa..bc9e05d 100644 --- a/internal/source/ai-brain/response.go +++ b/internal/source/ai-brain/response.go @@ -108,7 +108,7 @@ func msgNoAIAnswer(messageID string) api.Message { } } -func msgAIAnswer(run openai.Run, payload *Payload, response string, toolCalls map[string]struct{}) api.Message { +func msgAIAnswer(run openai.Run, payload *Payload, response string, toolCalls map[string]struct{}, isLastMessage bool) api.Message { var ( msgID = payload.MessageID btnBldr = api.NewMessageButtonBuilder() @@ -117,7 +117,7 @@ func msgAIAnswer(run openai.Run, payload *Payload, response string, toolCalls ma // MS Teams if strings.Contains(msgID, teamsMessageIDSubstr) { - return api.Message{ + teamsRes := api.Message{ Type: api.BasicCardWithButtonsInSeparateMsg, ParentActivityID: msgID, BaseBody: api.Body{ @@ -127,14 +127,20 @@ func msgAIAnswer(run openai.Run, payload *Payload, response string, toolCalls ma // which doesn't support most of the markdown elements. Plaintext: markdownToTeams(response, usedToolsMsg), }, - Sections: []api.Section{ + } + + if isLastMessage { + // add Report button + teamsRes.Sections = []api.Section{ { Buttons: api.Buttons{ btnBldr.ForCommandWithoutDesc(reportResponseBtnName, reportCmd(run, payload)), }, }, - }, + } } + + return teamsRes } // the Sections.Base.Body is rendered by engine using `fmt.Sprintf` so we need to escape '%' returned diff --git a/internal/source/ai-brain/response_test.go b/internal/source/ai-brain/response_test.go index 77a18cc..93329a6 100644 --- a/internal/source/ai-brain/response_test.go +++ b/internal/source/ai-brain/response_test.go @@ -26,14 +26,14 @@ func TestConvertProperlyAIAnswer(t *testing.T) { out := msgAIAnswer(openai.Run{}, &Payload{ MessageID: "42.42", Prompt: "This is a test", - }, string(md), nil) + }, string(md), nil, true) assertGolden(t, out.Sections[0].Base.Body.Plaintext, "slack.golden.md") // Teams out = msgAIAnswer(openai.Run{}, &Payload{ MessageID: "19:d25cbf7cbfa74d22b42a2918452e1153@thread.tacv2", Prompt: "This is a test", - }, string(md), nil) + }, string(md), nil, true) assertGolden(t, out.BaseBody.Plaintext, "teams.golden.md") } @@ -100,7 +100,7 @@ func TestConvertProperlyAIAnswerWithTools(t *testing.T) { out := msgAIAnswer(openai.Run{}, &Payload{ MessageID: "42.42", Prompt: "This is a test", - }, string(md), convertedToolCalls) + }, string(md), convertedToolCalls, true) outBytes, err := json.MarshalIndent(out, "", " ") require.NoError(t, err) @@ -110,16 +110,24 @@ func TestConvertProperlyAIAnswerWithTools(t *testing.T) { out = msgAIAnswer(openai.Run{}, &Payload{ MessageID: "19:d25cbf7cbfa74d22b42a2918452e1153@thread.tacv2", Prompt: "This is a test", - }, string(md), convertedToolCalls) + }, string(md), convertedToolCalls, true) outBytes, err = json.MarshalIndent(out, "", " ") require.NoError(t, err) assertGolden(t, string(outBytes), "teams-tools.golden.json") + out = msgAIAnswer(openai.Run{}, &Payload{ + MessageID: "19:d25cbf7cbfa74d22b42a2918452e1153@thread.tacv2", + Prompt: "This is a test two", + }, string(md), convertedToolCalls, false) + outBytes, err = json.MarshalIndent(out, "", " ") + require.NoError(t, err) + assertGolden(t, string(outBytes), "teams-tools-no-report.golden.json") + // Discord and others out = msgAIAnswer(openai.Run{}, &Payload{ MessageID: "", Prompt: "This is a test", - }, string(md), convertedToolCalls) + }, string(md), convertedToolCalls, true) outBytes, err = json.MarshalIndent(out, "", " ") require.NoError(t, err) assertGolden(t, string(outBytes), "discord-tools.golden.json") diff --git a/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/teams-tools-no-report.golden.json b/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/teams-tools-no-report.golden.json new file mode 100644 index 0000000..25263d4 --- /dev/null +++ b/internal/source/ai-brain/testdata/TestConvertProperlyAIAnswerWithTools/teams-tools-no-report.golden.json @@ -0,0 +1,8 @@ +{ + "type": "basicCardWithButtonsInSeparateMessage", + "baseBody": { + "plaintext": "**Found Issues**\n\nThe issue you're facing is due to a missing secret named `ngin`. Secrets in Kubernetes are used to store and handle sensitive information, such as passwords, OAuth tokens, ssh keys, etc. The specific error indicates that a pod is trying to use a secret called `ngin` that doesn’t exist in the cluster. Here’s a simple step-by-step guide on how to fix it:\n\n1. **Identify the Missing Secret Requirement:**\n\n \u003e Ensure that the name **\"ngin\"** is correct. Sometimes, a typo in the pod configuration or secret name can cause such issues.\n\n2. Create the Secret:\n If the secret is indeed missing `##` and you know the data that should be within it (like a token or password), you will need to create the secret. This can be done using the kubectl command line. For example, if you're creating a generic secret, you might use:\n\n ```shell\n kubectl create secret generic ngin --from-literal=key=value -n test\n ```\n\n Replace key and value with the actual data you need to store. The -n test specifies the namespace (test in this case) where the secret will be created. Adjust as necessary.\n3. **Update the Pod or Deployment Configuration:**\n If the secret name was incorrect due to a typo in your pod or deployment configuration, you should update the relevant YAML file to correct the secret name and then apply the changes. For example:\n\n ```yaml\n apiVersion: v1\n kind: Pod\n metadata:\n name: your-pod-name\n spec:\n containers:\n - name: your-container-name\n image: nginx\n env:\n - name: SECRET_KEY\n valueFrom:\n secretKeyRef:\n name: ngin # Make sure this matches the correct secret name\n key: key\n ```\n\nIf you had to update your configuration, apply the changes:\n\n```\nkubectl apply -f your-deployment.yaml\n```\n\n**Ecosystem and Community**\n\n[logo](https://raw.githubusercontent.com/kubeshop/botkube/main/branding/logos/botkube-black-32x32.png)\n\n_Kubernetes_ has a ~~large~~, rapidly growing `ecosystem`. Kubernetes' services, support, and tools are widely available.\n\n\u003e Kubernetes has entered the chat\n\u003e Intelligent Kubernetes Monitoring \u0026 Troubleshooting Platform\n\n[Botkube](https://botkube.io/)\n\n\n| Tables | Are | Cool |\n|----------|:-------------:|------:|\n| col 1 is | left-aligned | $1600 |\n| col 2 is | centered | $12 |\n| col 3 is | right-aligned | $1 |\n\n\n**Notes**\n\nThis message includes:\n- link\n- image\n- text \n - italic\n - bold\n - strike\n- multi-line code block\n - with syntax\n - without syntax\n- single code block\n- bullet list\n- numbered list\n- headers\n- blockquote\n- table\n\n\n~ℹ️ **Sources used:** Botkube agent configuration | Botkube agent status | Botkube and custom content search | kubectl get | kubectl logs~\n\n~AI-generated content may be incorrect.~\n" + }, + "timestamp": "0001-01-01T00:00:00Z", + "parentActivityId": "19:d25cbf7cbfa74d22b42a2918452e1153@thread.tacv2" +} \ No newline at end of file diff --git a/internal/source/ai-brain/tool_calls.go b/internal/source/ai-brain/tool_calls.go index 644206c..7a92023 100644 --- a/internal/source/ai-brain/tool_calls.go +++ b/internal/source/ai-brain/tool_calls.go @@ -20,6 +20,10 @@ var userFriendlyToolNames = map[string]string{ "function/kubectlTopPods": "kubectl top pods", "function/kubectlTopNodes": "kubectl top nodes", "function/kubectlLogs": "kubectl logs", + "function/kubescapeScanCluster": "Kubescape cluster scan", + "function/kubescapeScanWorkload": "Kubescape workload scan", + "function/kubescapeScanImage": "Kubescape image scan", + "function/kubescapeScanControl": "Kubescape control scan", "code_interpreter": "Code Interpreter", // we don't use this one, at least yet } From 778c5630c97b7f1fc6e1beba04c5159e6c9d11bc Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Tue, 9 Jul 2024 09:37:45 +0200 Subject: [PATCH 2/2] Fix error wrapping and logging --- internal/source/ai-brain/kubescape_tools.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/source/ai-brain/kubescape_tools.go b/internal/source/ai-brain/kubescape_tools.go index 447d537..d48aaad 100644 --- a/internal/source/ai-brain/kubescape_tools.go +++ b/internal/source/ai-brain/kubescape_tools.go @@ -87,7 +87,7 @@ func (k *KubescapeRunner) ScanControl(ctx context.Context, rawArgs []byte, _ *Pa ctx, span := k.tracer.Start(ctx, "aibrain.KubescapeRunner.ScanControl") defer span.End() - span.SetAttributes(attribute.String("kubescapeRunner.args", string(rawArgs))) + span.SetAttributes(attribute.String("KubescapeRunner.args", string(rawArgs))) var args scanControlArgs if err := json.Unmarshal(rawArgs, &args); err != nil { @@ -99,7 +99,7 @@ func (k *KubescapeRunner) ScanControl(ctx context.Context, rawArgs []byte, _ *Pa } func (k *KubescapeRunner) runKubescapeCommand(ctx context.Context, cmd string) (string, error) { - ctx, span := k.tracer.Start(ctx, "aibrain.KubectlRunner.runKubescapeCommand") + ctx, span := k.tracer.Start(ctx, "aibrain.KubescapeRunner.runKubescapeCommand") defer span.End() envs := map[string]string{ @@ -107,12 +107,12 @@ func (k *KubescapeRunner) runKubescapeCommand(ctx context.Context, cmd string) ( } cmd = fmt.Sprintf("%s %s", kubescapeBinaryName, cmd) - span.SetAttributes(attribute.String("kubectlRunner.cmd", cmd)) + span.SetAttributes(attribute.String("KubescapeRunner.cmd", cmd)) out, err := plugin.ExecuteCommand(ctx, cmd, plugin.ExecuteCommandEnvs(envs)) if out.ExitCode != 0 { - return fmt.Sprintf("kubectl command failed: %s", color.ClearCode(out.CombinedOutput())), nil + return fmt.Sprintf("kubescape command failed: %s", color.ClearCode(out.CombinedOutput())), nil } if err != nil {