diff --git a/api/openapispec/docs.go b/api/openapispec/docs.go index ba3edcc8..edb71689 100644 --- a/api/openapispec/docs.go +++ b/api/openapispec/docs.go @@ -376,6 +376,70 @@ var doc = `{ } } }, + "/insight/issue/interpret/stream": { + "post": { + "description": "This endpoint analyzes scanner issues using AI to provide detailed interpretation and insights", + "consumes": [ + "application/json" + ], + "produces": [ + "text/event-stream" + ], + "tags": [ + "insight" + ], + "summary": "Interpret scanner issues using AI", + "parameters": [ + { + "description": "The audit data to interpret", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/scanner.InterpretRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/ai.InterpretEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, "/insight/yaml/interpret/stream": { "post": { "description": "This endpoint analyzes YAML content using AI to provide detailed interpretation and insights", @@ -413,6 +477,24 @@ var doc = `{ "type": "string" } }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "type": "string" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -969,7 +1051,7 @@ var doc = `{ "200": { "description": "Audit results", "schema": { - "$ref": "#/definitions/scanner.AuditData" + "$ref": "#/definitions/ai.AuditData" } }, "400": { @@ -2067,6 +2149,29 @@ var doc = `{ } } }, + "ai.AuditData": { + "type": "object", + "properties": { + "bySeverity": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "issueGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/ai.IssueGroup" + } + }, + "issueTotal": { + "type": "integer" + }, + "resourceTotal": { + "type": "integer" + } + } + }, "ai.DiagnosisEvent": { "type": "object", "properties": { @@ -2116,6 +2221,20 @@ var doc = `{ } } }, + "ai.IssueGroup": { + "type": "object", + "properties": { + "issue": { + "$ref": "#/definitions/scanner.Issue" + }, + "resourceGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.ResourceGroup" + } + } + } + }, "cluster.ClusterPayload": { "type": "object", "properties": { @@ -2296,26 +2415,16 @@ var doc = `{ } } }, - "scanner.AuditData": { + "scanner.InterpretRequest": { "type": "object", "properties": { - "bySeverity": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "issueGroups": { - "type": "array", - "items": { - "$ref": "#/definitions/scanner.IssueGroup" - } - }, - "issueTotal": { - "type": "integer" + "auditData": { + "description": "The audit data to interpret", + "$ref": "#/definitions/ai.AuditData" }, - "resourceTotal": { - "type": "integer" + "language": { + "description": "Language for interpretation", + "type": "string" } } }, @@ -2340,20 +2449,6 @@ var doc = `{ } } }, - "scanner.IssueGroup": { - "type": "object", - "properties": { - "issue": { - "$ref": "#/definitions/scanner.Issue" - }, - "resourceGroups": { - "type": "array", - "items": { - "$ref": "#/definitions/entity.ResourceGroup" - } - } - } - }, "unstructured.Unstructured": { "type": "object", "properties": { diff --git a/api/openapispec/swagger.json b/api/openapispec/swagger.json index 7ce54ade..5262536c 100644 --- a/api/openapispec/swagger.json +++ b/api/openapispec/swagger.json @@ -360,6 +360,70 @@ } } }, + "/insight/issue/interpret/stream": { + "post": { + "description": "This endpoint analyzes scanner issues using AI to provide detailed interpretation and insights", + "consumes": [ + "application/json" + ], + "produces": [ + "text/event-stream" + ], + "tags": [ + "insight" + ], + "summary": "Interpret scanner issues using AI", + "parameters": [ + { + "description": "The audit data to interpret", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/scanner.InterpretRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/ai.InterpretEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, "/insight/yaml/interpret/stream": { "post": { "description": "This endpoint analyzes YAML content using AI to provide detailed interpretation and insights", @@ -397,6 +461,24 @@ "type": "string" } }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "type": "string" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -953,7 +1035,7 @@ "200": { "description": "Audit results", "schema": { - "$ref": "#/definitions/scanner.AuditData" + "$ref": "#/definitions/ai.AuditData" } }, "400": { @@ -2051,6 +2133,29 @@ } } }, + "ai.AuditData": { + "type": "object", + "properties": { + "bySeverity": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "issueGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/ai.IssueGroup" + } + }, + "issueTotal": { + "type": "integer" + }, + "resourceTotal": { + "type": "integer" + } + } + }, "ai.DiagnosisEvent": { "type": "object", "properties": { @@ -2100,6 +2205,20 @@ } } }, + "ai.IssueGroup": { + "type": "object", + "properties": { + "issue": { + "$ref": "#/definitions/scanner.Issue" + }, + "resourceGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.ResourceGroup" + } + } + } + }, "cluster.ClusterPayload": { "type": "object", "properties": { @@ -2280,26 +2399,16 @@ } } }, - "scanner.AuditData": { + "scanner.InterpretRequest": { "type": "object", "properties": { - "bySeverity": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "issueGroups": { - "type": "array", - "items": { - "$ref": "#/definitions/scanner.IssueGroup" - } - }, - "issueTotal": { - "type": "integer" + "auditData": { + "description": "The audit data to interpret", + "$ref": "#/definitions/ai.AuditData" }, - "resourceTotal": { - "type": "integer" + "language": { + "description": "Language for interpretation", + "type": "string" } } }, @@ -2324,20 +2433,6 @@ } } }, - "scanner.IssueGroup": { - "type": "object", - "properties": { - "issue": { - "$ref": "#/definitions/scanner.Issue" - }, - "resourceGroups": { - "type": "array", - "items": { - "$ref": "#/definitions/entity.ResourceGroup" - } - } - } - }, "unstructured.Unstructured": { "type": "object", "properties": { diff --git a/api/openapispec/swagger.yaml b/api/openapispec/swagger.yaml index b296749a..41e2c8d1 100644 --- a/api/openapispec/swagger.yaml +++ b/api/openapispec/swagger.yaml @@ -27,6 +27,21 @@ definitions: timestamp: type: string type: object + ai.AuditData: + properties: + bySeverity: + additionalProperties: + type: integer + type: object + issueGroups: + items: + $ref: '#/definitions/ai.IssueGroup' + type: array + issueTotal: + type: integer + resourceTotal: + type: integer + type: object ai.DiagnosisEvent: properties: content: @@ -60,6 +75,15 @@ definitions: description: 'Event type: start, chunk, error, complete' type: string type: object + ai.IssueGroup: + properties: + issue: + $ref: '#/definitions/scanner.Issue' + resourceGroups: + items: + $ref: '#/definitions/entity.ResourceGroup' + type: array + type: object cluster.ClusterPayload: properties: description: @@ -189,20 +213,14 @@ definitions: name: type: string type: object - scanner.AuditData: + scanner.InterpretRequest: properties: - bySeverity: - additionalProperties: - type: integer - type: object - issueGroups: - items: - $ref: '#/definitions/scanner.IssueGroup' - type: array - issueTotal: - type: integer - resourceTotal: - type: integer + auditData: + $ref: '#/definitions/ai.AuditData' + description: The audit data to interpret + language: + description: Language for interpretation + type: string type: object scanner.Issue: properties: @@ -221,15 +239,6 @@ definitions: description: Title is a brief summary of the issue. type: string type: object - scanner.IssueGroup: - properties: - issue: - $ref: '#/definitions/scanner.Issue' - resourceGroups: - items: - $ref: '#/definitions/entity.ResourceGroup' - type: array - type: object unstructured.Unstructured: properties: object: @@ -487,6 +496,49 @@ paths: summary: Stream pod logs using Server-Sent Events tags: - insight + /insight/issue/interpret/stream: + post: + consumes: + - application/json + description: This endpoint analyzes scanner issues using AI to provide detailed + interpretation and insights + parameters: + - description: The audit data to interpret + in: body + name: request + required: true + schema: + $ref: '#/definitions/scanner.InterpretRequest' + produces: + - text/event-stream + responses: + "200": + description: OK + schema: + $ref: '#/definitions/ai.InterpretEvent' + "400": + description: Bad Request + schema: + type: string + "401": + description: Unauthorized + schema: + type: string + "404": + description: Not Found + schema: + type: string + "429": + description: Too Many Requests + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Interpret scanner issues using AI + tags: + - insight /insight/yaml/interpret/stream: post: consumes: @@ -511,6 +563,18 @@ paths: description: Bad Request schema: type: string + "401": + description: Unauthorized + schema: + type: string + "404": + description: Not Found + schema: + type: string + "429": + description: Too Many Requests + schema: + type: string "500": description: Internal Server Error schema: @@ -883,7 +947,7 @@ paths: "200": description: Audit results schema: - $ref: '#/definitions/scanner.AuditData' + $ref: '#/definitions/ai.AuditData' "400": description: Bad Request schema: diff --git a/docs/api.md b/docs/api.md index 7033582d..b38389af 100644 --- a/docs/api.md +++ b/docs/api.md @@ -77,6 +77,7 @@ Karpor is a brand new Kubernetes visualization tool that focuses on search, insi | GET | /rest-api/v1/insight/topology | [get rest API v1 insight topology](#get-rest-api-v1-insight-topology) | GetTopology returns a topology map for a Kubernetes resource by name, namespace, cluster, apiVersion and kind. | | POST | /insight/aggregator/event/diagnosis/stream | [post insight aggregator event diagnosis stream](#post-insight-aggregator-event-diagnosis-stream) | Diagnose events using AI | | POST | /insight/aggregator/log/diagnosis/stream | [post insight aggregator log diagnosis stream](#post-insight-aggregator-log-diagnosis-stream) | Diagnose pod logs using AI | +| POST | /insight/issue/interpret/stream | [post insight issue interpret stream](#post-insight-issue-interpret-stream) | Interpret scanner issues using AI | | POST | /insight/yaml/interpret/stream | [post insight yaml interpret stream](#post-insight-yaml-interpret-stream) | Interpret YAML using AI | @@ -793,7 +794,7 @@ Status: OK -[ScannerAuditData](#scanner-audit-data) +[AiAuditData](#ai-audit-data) ##### 400 - Bad Request Status: Bad Request @@ -1906,6 +1907,93 @@ Status: Internal Server Error +### Interpret scanner issues using AI (*PostInsightIssueInterpretStream*) + +``` +POST /insight/issue/interpret/stream +``` + +This endpoint analyzes scanner issues using AI to provide detailed interpretation and insights + +#### Consumes + * application/json + +#### Produces + * text/event-stream + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +|------|--------|------|---------|-----------| :------: |---------|-------------| +| request | `body` | [ScannerInterpretRequest](#scanner-interpret-request) | `models.ScannerInterpretRequest` | | ✓ | | The audit data to interpret | + +#### All responses +| Code | Status | Description | Has headers | Schema | +|------|--------|-------------|:-----------:|--------| +| [200](#post-insight-issue-interpret-stream-200) | OK | OK | | [schema](#post-insight-issue-interpret-stream-200-schema) | +| [400](#post-insight-issue-interpret-stream-400) | Bad Request | Bad Request | | [schema](#post-insight-issue-interpret-stream-400-schema) | +| [401](#post-insight-issue-interpret-stream-401) | Unauthorized | Unauthorized | | [schema](#post-insight-issue-interpret-stream-401-schema) | +| [404](#post-insight-issue-interpret-stream-404) | Not Found | Not Found | | [schema](#post-insight-issue-interpret-stream-404-schema) | +| [429](#post-insight-issue-interpret-stream-429) | Too Many Requests | Too Many Requests | | [schema](#post-insight-issue-interpret-stream-429-schema) | +| [500](#post-insight-issue-interpret-stream-500) | Internal Server Error | Internal Server Error | | [schema](#post-insight-issue-interpret-stream-500-schema) | + +#### Responses + + +##### 200 - OK +Status: OK + +###### Schema + + + +[AiInterpretEvent](#ai-interpret-event) + +##### 400 - Bad Request +Status: Bad Request + +###### Schema + + + + + +##### 401 - Unauthorized +Status: Unauthorized + +###### Schema + + + + + +##### 404 - Not Found +Status: Not Found + +###### Schema + + + + + +##### 429 - Too Many Requests +Status: Too Many Requests + +###### Schema + + + + + +##### 500 - Internal Server Error +Status: Internal Server Error + +###### Schema + + + + + ### Interpret YAML using AI (*PostInsightYamlInterpretStream*) ``` @@ -1931,6 +2019,9 @@ This endpoint analyzes YAML content using AI to provide detailed interpretation |------|--------|-------------|:-----------:|--------| | [200](#post-insight-yaml-interpret-stream-200) | OK | OK | | [schema](#post-insight-yaml-interpret-stream-200-schema) | | [400](#post-insight-yaml-interpret-stream-400) | Bad Request | Bad Request | | [schema](#post-insight-yaml-interpret-stream-400-schema) | +| [401](#post-insight-yaml-interpret-stream-401) | Unauthorized | Unauthorized | | [schema](#post-insight-yaml-interpret-stream-401-schema) | +| [404](#post-insight-yaml-interpret-stream-404) | Not Found | Not Found | | [schema](#post-insight-yaml-interpret-stream-404-schema) | +| [429](#post-insight-yaml-interpret-stream-429) | Too Many Requests | Too Many Requests | | [schema](#post-insight-yaml-interpret-stream-429-schema) | | [500](#post-insight-yaml-interpret-stream-500) | Internal Server Error | Internal Server Error | | [schema](#post-insight-yaml-interpret-stream-500-schema) | #### Responses @@ -1954,6 +2045,33 @@ Status: Bad Request +##### 401 - Unauthorized +Status: Unauthorized + +###### Schema + + + + + +##### 404 - Not Found +Status: Not Found + +###### Schema + + + + + +##### 429 - Too Many Requests +Status: Too Many Requests + +###### Schema + + + + + ##### 500 - Internal Server Error Status: Internal Server Error @@ -2556,6 +2674,24 @@ Status: Internal Server Error +### ai.AuditData + + + + + + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +|------|------|---------|:--------:| ------- |-------------|---------| +| bySeverity | map of integer| `map[string]int64` | | | | | +| issueGroups | [][AiIssueGroup](#ai-issue-group)| `[]*AiIssueGroup` | | | | | +| issueTotal | integer| `int64` | | | | | +| resourceTotal | integer| `int64` | | | | | + + + ### ai.DiagnosisEvent @@ -2608,6 +2744,22 @@ Status: Internal Server Error +### ai.IssueGroup + + + + + + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +|------|------|---------|:--------:| ------- |-------------|---------| +| issue | [ScannerIssue](#scanner-issue)| `ScannerIssue` | | | | | +| resourceGroups | [][EntityResourceGroup](#entity-resource-group)| `[]*EntityResourceGroup` | | | | | + + + ### cluster.ClusterPayload @@ -2789,7 +2941,7 @@ of issues across different severity categories. | | -### scanner.AuditData +### scanner.InterpretRequest @@ -2800,10 +2952,8 @@ of issues across different severity categories. | | | Name | Type | Go type | Required | Default | Description | Example | |------|------|---------|:--------:| ------- |-------------|---------| -| bySeverity | map of integer| `map[string]int64` | | | | | -| issueGroups | [][ScannerIssueGroup](#scanner-issue-group)| `[]*ScannerIssueGroup` | | | | | -| issueTotal | integer| `int64` | | | | | -| resourceTotal | integer| `int64` | | | | | +| auditData | [AiAuditData](#ai-audit-data)| `AiAuditData` | | | The audit data to interpret | | +| language | string| `string` | | | Language for interpretation | | @@ -2825,22 +2975,6 @@ of issues across different severity categories. | | -### scanner.IssueGroup - - - - - - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -|------|------|---------|:--------:| ------- |-------------|---------| -| issue | [ScannerIssue](#scanner-issue)| `ScannerIssue` | | | | | -| resourceGroups | [][EntityResourceGroup](#entity-resource-group)| `[]*EntityResourceGroup` | | | | | - - - ### unstructured.Unstructured diff --git a/pkg/core/handler/detail/interpret.go b/pkg/core/handler/detail/interpret.go index aad287f4..27142774 100644 --- a/pkg/core/handler/detail/interpret.go +++ b/pkg/core/handler/detail/interpret.go @@ -19,6 +19,7 @@ import ( "fmt" "net/http" + "github.com/KusionStack/karpor/pkg/core/handler" "github.com/KusionStack/karpor/pkg/core/manager/ai" "github.com/KusionStack/karpor/pkg/util/ctxutil" "k8s.io/apiserver/pkg/server" @@ -40,6 +41,9 @@ type InterpretRequest struct { // @Param request body InterpretRequest true "The YAML content to interpret" // @Success 200 {object} ai.InterpretEvent // @Failure 400 {string} string "Bad Request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 429 {string} string "Too Many Requests" +// @Failure 404 {string} string "Not Found" // @Failure 500 {string} string "Internal Server Error" // @Router /insight/yaml/interpret/stream [post] func InterpretYAML(aiMgr *ai.AIManager, c *server.CompletedConfig) http.HandlerFunc { @@ -48,16 +52,19 @@ func InterpretYAML(aiMgr *ai.AIManager, c *server.CompletedConfig) http.HandlerF ctx := r.Context() logger := ctxutil.GetLogger(ctx) + // Begin the interpretation process, logging the start + logger.Info("Starting YAML interpretation in handler ...") + // Parse request body var req InterpretRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, fmt.Sprintf("invalid request format: %v", err), http.StatusBadRequest) + handler.FailureRender(ctx, w, r, fmt.Errorf("invalid request format: %v", err)) return } // Validate request if req.YAML == "" { - http.Error(w, "YAML content is required", http.StatusBadRequest) + handler.FailureRender(ctx, w, r, fmt.Errorf("YAML content is required")) return } if req.Language == "" { @@ -73,7 +80,7 @@ func InterpretYAML(aiMgr *ai.AIManager, c *server.CompletedConfig) http.HandlerF flusher, ok := w.(http.Flusher) if !ok { - http.Error(w, "Streaming unsupported", http.StatusInternalServerError) + handler.FailureRender(ctx, w, r, fmt.Errorf("streaming unsupported")) return } diff --git a/pkg/core/handler/scanner/interpret.go b/pkg/core/handler/scanner/interpret.go new file mode 100644 index 00000000..2f1610ba --- /dev/null +++ b/pkg/core/handler/scanner/interpret.go @@ -0,0 +1,110 @@ +// Copyright The Karpor Authors. +// +// 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 scanner + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/KusionStack/karpor/pkg/core/handler" + "github.com/KusionStack/karpor/pkg/core/manager/ai" + "github.com/KusionStack/karpor/pkg/util/ctxutil" + "k8s.io/apiserver/pkg/server" +) + +// InterpretRequest represents the request body for issue interpretation +type InterpretRequest struct { + AuditData *ai.AuditData `json:"auditData"` // The audit data to interpret + Language string `json:"language"` // Language for interpretation +} + +// InterpretIssues returns an HTTP handler function that performs AI interpretation on scanner issues +// +// @Summary Interpret scanner issues using AI +// @Description This endpoint analyzes scanner issues using AI to provide detailed interpretation and insights +// @Tags insight +// @Accept json +// @Produce text/event-stream +// @Param request body InterpretRequest true "The audit data to interpret" +// @Success 200 {object} ai.InterpretEvent +// @Failure 400 {string} string "Bad Request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 429 {string} string "Too Many Requests" +// @Failure 404 {string} string "Not Found" +// @Failure 500 {string} string "Internal Server Error" +// @Router /insight/issue/interpret/stream [post] +func InterpretIssues(aiMgr *ai.AIManager, c *server.CompletedConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Extract the context and logger from the request + ctx := r.Context() + logger := ctxutil.GetLogger(ctx) + + // Begin the interpretation process, logging the start + logger.Info("Starting issue interpretation in handler ...") + + // Parse request body + var req InterpretRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + handler.FailureRender(ctx, w, r, fmt.Errorf("invalid request format: %v", err)) + return + } + + // Log successful decoding of the request body + logger.Info("Successfully decoded the request body", "auditData", req.AuditData) + + // Validate request + if req.AuditData == nil { + handler.FailureRender(ctx, w, r, fmt.Errorf("audit data is required")) + return + } + if req.Language == "" { + req.Language = "English" // Default to English if language not specified + } + + // Set headers for SSE + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("X-Accel-Buffering", "no") + + flusher, ok := w.(http.Flusher) + if !ok { + handler.FailureRender(ctx, w, r, fmt.Errorf("streaming unsupported")) + return + } + + // Create channel for interpretation events + eventChan := make(chan *ai.InterpretEvent, 10) + go func() { + if err := aiMgr.InterpretIssues(ctx, req.AuditData, req.Language, eventChan); err != nil { + logger.Error(err, "Failed to interpret issues") + // Error will be sent through eventChan + } + }() + + // Stream events to client + for event := range eventChan { + data, err := json.Marshal(event) + if err != nil { + logger.Error(err, "Failed to marshal event") + continue + } + fmt.Fprintf(w, "data: %s\n\n", data) + flusher.Flush() + } + } +} diff --git a/pkg/core/handler/scanner/scanner.go b/pkg/core/handler/scanner/scanner.go index 3621fe04..1ae7328b 100644 --- a/pkg/core/handler/scanner/scanner.go +++ b/pkg/core/handler/scanner/scanner.go @@ -20,8 +20,8 @@ import ( "github.com/KusionStack/karpor/pkg/core/entity" "github.com/KusionStack/karpor/pkg/core/handler" + _ "github.com/KusionStack/karpor/pkg/core/manager/ai" "github.com/KusionStack/karpor/pkg/core/manager/insight" - _ "github.com/KusionStack/karpor/pkg/infra/scanner" "github.com/KusionStack/karpor/pkg/util/ctxutil" ) @@ -31,18 +31,18 @@ import ( // @Description This endpoint audits based on the specified resource group. // @Tags insight // @Produce json -// @Param cluster query string false "The specified cluster name, such as 'example-cluster'" -// @Param apiVersion query string false "The specified apiVersion, such as 'apps/v1'" -// @Param kind query string false "The specified kind, such as 'Deployment'" -// @Param namespace query string false "The specified namespace, such as 'default'" -// @Param name query string false "The specified resource name, such as 'foo'" -// @Param forceNew query bool false "Switch for forced scanning, default is 'false'" -// @Success 200 {object} AuditData "Audit results" -// @Failure 400 {string} string "Bad Request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 429 {string} string "Too Many Requests" -// @Failure 404 {string} string "Not Found" -// @Failure 500 {string} string "Internal Server Error" +// @Param cluster query string false "The specified cluster name, such as 'example-cluster'" +// @Param apiVersion query string false "The specified apiVersion, such as 'apps/v1'" +// @Param kind query string false "The specified kind, such as 'Deployment'" +// @Param namespace query string false "The specified namespace, such as 'default'" +// @Param name query string false "The specified resource name, such as 'foo'" +// @Param forceNew query bool false "Switch for forced scanning, default is 'false'" +// @Success 200 {object} ai.AuditData "Audit results" +// @Failure 400 {string} string "Bad Request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 429 {string} string "Too Many Requests" +// @Failure 404 {string} string "Not Found" +// @Failure 500 {string} string "Internal Server Error" // @Router /rest-api/v1/insight/audit [get] func Audit(insight *insight.InsightManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/core/handler/scanner/types.go b/pkg/core/handler/scanner/types.go deleted file mode 100644 index 2e8f7bc4..00000000 --- a/pkg/core/handler/scanner/types.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright The Karpor Authors. -// -// 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 scanner - -import ( - "github.com/KusionStack/karpor/pkg/core/entity" - "github.com/KusionStack/karpor/pkg/infra/scanner" -) - -// AuditData represents the aggregated data of scanner issues, including the -// original list of issues and their aggregated count based on title. -type AuditData struct { - IssueTotal int `json:"issueTotal"` - ResourceTotal int `json:"resourceTotal"` - BySeverity map[string]int `json:"bySeverity"` - IssueGroups []*IssueGroup `json:"issueGroups"` -} - -// IssueGroup represents a group of resourceGroups tied to a specific issue. -type IssueGroup struct { - Issue scanner.Issue `json:"issue"` - ResourceGroups []entity.ResourceGroup `json:"resourceGroups"` -} diff --git a/pkg/core/handler/scanner/util.go b/pkg/core/handler/scanner/util.go index 97e09268..e223db06 100644 --- a/pkg/core/handler/scanner/util.go +++ b/pkg/core/handler/scanner/util.go @@ -18,19 +18,20 @@ import ( "sort" "github.com/KusionStack/karpor/pkg/core/entity" + "github.com/KusionStack/karpor/pkg/core/manager/ai" "github.com/KusionStack/karpor/pkg/infra/scanner" ) // convertScanResultToAuditData converts the scanner.ScanResult to an AuditData // structure containing aggregated issue and resource data. -func convertScanResultToAuditData(sr scanner.ScanResult) *AuditData { - issueGroups := make([]*IssueGroup, 0, len(sr.ByIssue())) +func convertScanResultToAuditData(sr scanner.ScanResult) *ai.AuditData { + issueGroups := make([]*ai.IssueGroup, 0, len(sr.ByIssue())) bySeverity := map[string]int{} // Iterate through each issue in the ScanResult and create corresponding // IssueGroup entries. for issue, resources := range sr.ByIssue() { - issueGroup := &IssueGroup{ + issueGroup := &ai.IssueGroup{ Issue: issue, ResourceGroups: []entity.ResourceGroup{}, } @@ -59,7 +60,7 @@ func convertScanResultToAuditData(sr scanner.ScanResult) *AuditData { }) // Construct the AuditData structure. - return &AuditData{ + return &ai.AuditData{ IssueTotal: sr.IssueTotal(), ResourceTotal: len(sr.ByResource()), BySeverity: bySeverity, diff --git a/pkg/core/manager/ai/issue.go b/pkg/core/manager/ai/issue.go new file mode 100644 index 00000000..6bc7d240 --- /dev/null +++ b/pkg/core/manager/ai/issue.go @@ -0,0 +1,134 @@ +// Copyright The Karpor Authors. +// +// 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 ai + +import ( + "context" + "fmt" + "strings" + + "github.com/KusionStack/karpor/pkg/core/entity" + "github.com/KusionStack/karpor/pkg/infra/scanner" +) + +// AuditData represents the aggregated data of scanner issues, including the +// original list of issues and their aggregated count based on title. +type AuditData struct { + IssueTotal int `json:"issueTotal"` + ResourceTotal int `json:"resourceTotal"` + BySeverity map[string]int `json:"bySeverity"` + IssueGroups []*IssueGroup `json:"issueGroups"` +} + +// IssueGroup represents a group of resourceGroups tied to a specific issue. +type IssueGroup struct { + Issue scanner.Issue `json:"issue"` + ResourceGroups []entity.ResourceGroup `json:"resourceGroups"` +} + +// InterpretIssues performs AI interpretation of scanner issues and sends events through the channel +func (a *AIManager) InterpretIssues(ctx context.Context, auditData *AuditData, language string, eventChan chan<- *InterpretEvent) error { + defer close(eventChan) + + // Send start event + eventChan <- &InterpretEvent{Type: "start"} + + // Validate input + if auditData == nil || len(auditData.IssueGroups) == 0 { + eventChan <- &InterpretEvent{ + Type: "error", + Content: "No issues to interpret", + } + return fmt.Errorf("no issues to interpret") + } + + // Build issue summary + var summary strings.Builder + summary.WriteString(fmt.Sprintf("Total Issues: %d\n", auditData.IssueTotal)) + summary.WriteString(fmt.Sprintf("Total Resources: %d\n", auditData.ResourceTotal)) + summary.WriteString("\nSeverity Distribution:\n") + for severity, count := range auditData.BySeverity { + summary.WriteString(fmt.Sprintf("- %s: %d\n", severity, count)) + } + + // Build issue details + summary.WriteString("\nIssue Details:\n") + for _, group := range auditData.IssueGroups { + summary.WriteString("\n## Issue\n") + summary.WriteString(fmt.Sprintf("Title: %s\n", group.Issue.Title)) + summary.WriteString(fmt.Sprintf("Severity: %s\n", group.Issue.Severity)) + summary.WriteString(fmt.Sprintf("Scanner: %s\n", group.Issue.Scanner)) + if group.Issue.Message != "" { + summary.WriteString(fmt.Sprintf("Message: %s\n", group.Issue.Message)) + } + + summary.WriteString("\nAffected Resources:\n") + for _, rg := range group.ResourceGroups { + summary.WriteString(fmt.Sprintf("- %s/%s (%s)\n", rg.Namespace, rg.Name, rg.Kind)) + } + } + + // Build prompt from template + prompt := fmt.Sprintf(ServicePromptMap[IssueInterpretType], language, summary.String()) + + // Get AI service client + if a.client == nil { + eventChan <- &InterpretEvent{ + Type: "error", + Content: "AI service not configured", + } + return fmt.Errorf("AI service not configured") + } + + // Stream completion from AI service + stream, err := a.client.GenerateStream(ctx, prompt) + if err != nil { + eventChan <- &InterpretEvent{ + Type: "error", + Content: fmt.Sprintf("Failed to start AI service: %v", err), + } + return fmt.Errorf("failed to start AI service: %w", err) + } + + // Process stream + var fullContent strings.Builder + for chunk := range stream { + select { + case <-ctx.Done(): + return ctx.Err() + default: + if strings.HasPrefix(chunk, "ERROR:") { + eventChan <- &InterpretEvent{ + Type: "error", + Content: fmt.Sprintf("AI service error: %v", strings.TrimPrefix(chunk, "ERROR: ")), + } + return fmt.Errorf("AI service error: %v", chunk) + } + + fullContent.WriteString(chunk) + eventChan <- &InterpretEvent{ + Type: "chunk", + Content: chunk, + } + } + } + + // Send complete event + eventChan <- &InterpretEvent{ + Type: "complete", + Content: fullContent.String(), + } + return nil +} diff --git a/pkg/core/manager/ai/prompt.go b/pkg/core/manager/ai/prompt.go index cf8f7dda..a731c849 100644 --- a/pkg/core/manager/ai/prompt.go +++ b/pkg/core/manager/ai/prompt.go @@ -30,6 +30,8 @@ const ( EventDiagnosisType PromptType = "event_diagnosis" // YAMLInterpretType represents the prompt type for YAML interpretation YAMLInterpretType PromptType = "yaml_interpret" + // IssueInterpretType represents the prompt type for issue interpretation + IssueInterpretType PromptType = "issue_interpret" ) var ServicePromptMap = map[PromptType]string{ @@ -181,4 +183,20 @@ Note: - Format your response with clear sections using markdown headings (##) and bullet points - Do NOT wrap your entire response in a markdown code block - Use code blocks only for YAML examples or specific configuration snippets`, + + IssueInterpretType: `You are a Kubernetes expert specialized in analyzing security issues and providing solutions. +Please analyze the following issues and provide your insights in %s. + +Issues Summary: +%s + +Please provide a concise analysis focusing on: +1. Brief summary of the most critical issues (1-2 sentences) +2. Detailed solutions with specific examples, including: + - Exact code or configuration changes needed + - Before and after examples + - Common pitfalls to avoid +3. Best practices and preventive measures + +Note: Format your response with clear sections using markdown headings (##) and bullet points. Do NOT wrap your entire response in a markdown code block.`, } diff --git a/pkg/core/route/route.go b/pkg/core/route/route.go index e4766897..dc79aaec 100644 --- a/pkg/core/route/route.go +++ b/pkg/core/route/route.go @@ -176,6 +176,7 @@ func setupRestAPIV1( r.Post("/aggregator/log/diagnosis/stream", aggregatorhandler.DiagnosePodLogs(aiMgr, genericConfig)) r.Post("/aggregator/event/diagnosis/stream", aggregatorhandler.DiagnoseEvents(aiMgr, genericConfig)) r.Post("/yaml/interpret/stream", detailhandler.InterpretYAML(aiMgr, genericConfig)) + r.Post("/issue/interpret/stream", scannerhandler.InterpretIssues(aiMgr, genericConfig)) }) r.Route("/resource-group-rule", func(r chi.Router) { diff --git a/ui/src/components/yaml/index.tsx b/ui/src/components/yaml/index.tsx index af332ba2..7b15650f 100644 --- a/ui/src/components/yaml/index.tsx +++ b/ui/src/components/yaml/index.tsx @@ -401,13 +401,6 @@ const Yaml = (props: IProps) => { clear: 'both', }} /> -
{t('ExceptionList.InterpretInProgress')}
+