From 47a2b22d7d9f6befda7fb60fc622b72ef4c40891 Mon Sep 17 00:00:00 2001 From: Berkay Berabi Date: Tue, 17 Dec 2024 15:10:33 +0100 Subject: [PATCH 1/9] feat/ai/explain: add the first implementation of GenerateAIExplanation Command in LSP --- README.md | 19 ++- application/server/server.go | 1 + application/server/server_test.go | 2 + domain/ide/command/command_factory.go | 7 + domain/ide/command/generate_ai_explanation.go | 95 +++++++++++ infrastructure/code/ai_explain.go | 153 ++++++++++++++++++ .../code/fake_snyk_code_api_service.go | 4 + infrastructure/code/snyk_code_http_client.go | 59 +++++++ .../code/snyk_code_http_client_interface.go | 13 ++ infrastructure/code/types.go | 21 +++ internal/types/command.go | 1 + 11 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 domain/ide/command/generate_ai_explanation.go create mode 100644 infrastructure/code/ai_explain.go diff --git a/README.md b/README.md index fcebb3ed2..7b7c9779c 100644 --- a/README.md +++ b/README.md @@ -380,6 +380,21 @@ Right now the language server supports the following actions: // WHEN comp.changePassword(); ``` +- `Generate AI Explanation` allows to retrieve explainations for various use-cases. + - command: `snyk.generateAIExplanation` + - args: + + - folderURI string + - fileURI string + - diff string + - issueID string (UUID) + - returns an array of explanations: + ```json5 + [{ + "explainId": "123", + "explanations": "bla bla explained" + }] + ``` - `Feature Flag Status Command` triggers the api call to check if a feature flag is enabled - command: `snyk.getFeatureFlagStatus` - args: @@ -393,8 +408,8 @@ Right now the language server supports the following actions: ``` - `Clear Cache` Clears either persisted or inMemory Cache or both. - command: `snyk.clearCache` - - args: - - `folderUri` string, + - args: + - `folderUri` string, - `cacheType` `persisted` or `inMemory` - `Generate Issue Description` Generates issue description in HTML. - command: `snyk.generateIssueDescription` diff --git a/application/server/server.go b/application/server/server.go index c3dedb65e..133f2aaa7 100644 --- a/application/server/server.go +++ b/application/server/server.go @@ -303,6 +303,7 @@ func initializeHandler(srv *jrpc2.Server) handler.Func { types.ClearCacheCommand, types.GenerateIssueDescriptionCommand, types.ReportAnalyticsCommand, + types.GenerateAIExplanationCommand, }, }, }, diff --git a/application/server/server_test.go b/application/server/server_test.go index e2392f465..6ee9995c0 100644 --- a/application/server/server_test.go +++ b/application/server/server_test.go @@ -266,6 +266,8 @@ func Test_initialize_shouldSupportAllCommands(t *testing.T) { assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.CodeFixCommand) assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.CodeSubmitFixFeedback) assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.CodeFixDiffsCommand) + assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.GenerateAIExplanationCommand) + assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.GenerateIssueDescriptionCommand) assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.ExecuteCLICommand) } diff --git a/domain/ide/command/command_factory.go b/domain/ide/command/command_factory.go index 61af048ba..2b0848ee7 100644 --- a/domain/ide/command/command_factory.go +++ b/domain/ide/command/command_factory.go @@ -88,6 +88,13 @@ func CreateFromCommandData( issueProvider: issueProvider, notifier: notifier, }, nil + case types.GenerateAIExplanationCommand: + return &generateAIExplanation{ + command: commandData, + codeScanner: codeScanner, + issueProvider: issueProvider, + notifier: notifier, + }, nil case types.ExecuteCLICommand: return &executeCLICommand{command: commandData, authService: authService, notifier: notifier, logger: c.Logger(), cli: cli}, nil case types.ClearCacheCommand: diff --git a/domain/ide/command/generate_ai_explanation.go b/domain/ide/command/generate_ai_explanation.go new file mode 100644 index 000000000..e29ce7ce0 --- /dev/null +++ b/domain/ide/command/generate_ai_explanation.go @@ -0,0 +1,95 @@ +/* + * © 2023-2024 Snyk Limited + * + * 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 command + + +import ( + "context" + "errors" + "path/filepath" + "strings" + + "github.com/sourcegraph/go-lsp" + + "github.com/snyk/snyk-ls/application/config" + "github.com/snyk/snyk-ls/domain/snyk" + "github.com/snyk/snyk-ls/infrastructure/code" + "github.com/snyk/snyk-ls/internal/notification" + "github.com/snyk/snyk-ls/internal/types" + uri2 "github.com/snyk/snyk-ls/internal/uri" +) + + +type generateAIExplanation struct { + command types.CommandData + notifier notification.Notifier + issueProvider snyk.IssueProvider + codeScanner *code.Scanner +} + +func (cmd *generateAIExplanation) Command() types.CommandData { + return cmd.command +} + +func (cmd *generateAIExplanation) Execute (ctx context.Context) (any, error) { + logger := config.CurrentConfig().Logger().With().Str("method", "generateAIExplanation.Execute").Logger() + + args := cmd.command.Arguments + if len(args) < 3 { + return nil, errors.New("missing required arguments") + } + + folderURI, ok := args[0].(string) + if !ok { + return nil, errors.New("failed to parse folder path") + } + folderPath := uri2.PathFromUri(lsp.DocumentURI(folderURI)) + + issueURI, ok := args[1].(string) + if !ok { + return nil, errors.New("failed to parse filepath") + } + + issuePath := uri2.PathFromUri(lsp.DocumentURI(issueURI)) + + relPath, err := filepath.Rel(folderPath, issuePath) + if err != nil { + return nil, err + } + + if strings.HasPrefix(relPath, "..") { + return nil, errors.New("issue path is not within the folder path") + } + + id, ok := args[2].(string) + if !ok { + return nil, errors.New("failed to parse issue id") + } + + issue := cmd.issueProvider.Issue(id) + if issue.ID == "" { + return nil, errors.New("failed to find issue") + } + + // Now we need to call cmd.codeScanner.GetAIExplanation + explanation, err := cmd.codeScanner.GetAIExplanation(ctx, folderPath, relPath, issue) + if err != nil { + logger.Err(err).Msgf("received an error from API: %s", err.Error()) + return explanation, err + } + return explanation, nil +} diff --git a/infrastructure/code/ai_explain.go b/infrastructure/code/ai_explain.go new file mode 100644 index 000000000..98419abd3 --- /dev/null +++ b/infrastructure/code/ai_explain.go @@ -0,0 +1,153 @@ +/* + * © 2024 Snyk Limited + * + * 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 code + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/snyk/snyk-ls/application/config" + "github.com/snyk/snyk-ls/domain/snyk" + performance2 "github.com/snyk/snyk-ls/internal/observability/performance" +) + + + + + +func (sc *Scanner) GetAIExplanation( + ctx context.Context, + baseDir string, + filePath string, + issue snyk.Issue, +) (explanation string, err error) { + method := "GetAIExplanation" + logger := config.CurrentConfig().Logger().With().Str("method", method).Logger() + span := sc.BundleUploader.instrumentor.StartSpan(ctx, method) + defer sc.BundleUploader.instrumentor.Finish(span) + + codeClient := sc.BundleUploader.SnykCode + sc.bundleHashesMutex.RLock() + bundleHash, found := sc.bundleHashes[baseDir] + sc.bundleHashesMutex.RUnlock() + if !found { + return explanation, fmt.Errorf("bundle hash not found for baseDir: %s", baseDir) + } + + encodedNormalizedPath, err := ToEncodedNormalizedPath(baseDir, filePath) + if err != nil { + return explanation, err + } + + options := ExplainOptions{ + bundleHash: bundleHash, + shardKey: getShardKey(baseDir, config.CurrentConfig().Token()), + filePath: encodedNormalizedPath, + issue: issue, + } + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + // timeoutTimer sends a trigger after 2 minutes to its channel + timeoutTimer := time.NewTimer(2 * time.Minute) + defer timeoutTimer.Stop() + for { + select { + case <-timeoutTimer.C: + const msg = "Timeout waiting for explanation." + logger.Error().Msg(msg) + return "", errors.New(msg) + case <-ticker.C: + explanation, explainStatus, explanationErr := codeClient.GetAIExplanation(span.Context(), baseDir, options) + if explanationErr != nil { + logger.Err(explanationErr).Msg("Error getting explanation") + return "", explanationErr + } else if explainStatus == completeStatus { + return explanation, nil + } + // If err == nil and fixStatus.message != completeStatus, we will keep polling. + } + } +} + + +func (s *SnykCodeHTTPClient) GetAIExplanation(ctx context.Context, basedir string, options ExplainOptions) ( + explanation string, + status string, + err error, +) { + method := "GetAIExplanation" + span := s.instrumentor.StartSpan(ctx, method) + defer s.instrumentor.Finish(span) + logger := config.CurrentConfig().Logger().With().Str("method", method).Logger() + logger.Info().Msg("Started obtaining AI explanation") + defer logger.Info().Msg("Finished obtaining AI explanation") + + explainResponse, err := s.getExplainResponse(ctx, options) + if err != nil { + return "", status, err + } + return explainResponse.Explanation, "COMPLETED", nil +} + +func (s *SnykCodeHTTPClient) getExplainResponse(ctx context.Context, options ExplainOptions) (explainResponse ExplainResponse, err error) { + method := "getExplainResponse" + + span := s.instrumentor.StartSpan(ctx, method) + defer s.instrumentor.Finish(span) + logger := config.CurrentConfig().Logger().With().Str("method", method).Logger() + + requestId, err := performance2.GetTraceId(ctx) + if err != nil { + logger.Err(err).Msg(failedToObtainRequestIdString + err.Error()) + return explainResponse, err + } + logger.Info().Str("requestId", requestId).Msg("Started obtaining explain Response") + defer logger.Info().Str("requestId", requestId).Msg("Finished obtaining explain Response") + + response, err := s.RunExplain(span.Context(), options) + if err != nil { + return response, err + } + + logger.Debug().Msgf("Status: %s", response.Status) + + if response.Status == "FAILED" { + logger.Error().Str("responseStatus", response.Status).Msg("explain failed") + return response, errors.New("Explain failed") + } + + if response.Status == "" { + logger.Error().Str("responseStatus", response.Status).Msg("unknown response status (empty)") + return response, errors.New("Unknown response status (empty)") + } + + if response.Status != completeStatus { + return response, nil + } + + return response, nil +} + + + + + + + diff --git a/infrastructure/code/fake_snyk_code_api_service.go b/infrastructure/code/fake_snyk_code_api_service.go index fe20d7dad..4efb1ed87 100644 --- a/infrastructure/code/fake_snyk_code_api_service.go +++ b/infrastructure/code/fake_snyk_code_api_service.go @@ -147,6 +147,10 @@ func (f *FakeSnykCodeClient) GetAutofixDiffs(_ context.Context, _ string, _ Auto return f.UnifiedDiffSuggestions, f.AutofixStatus, nil } +func (f *FakeSnykCodeClient) GetAIExplanation(_ context.Context, _ string, _ ExplainOptions) (explanation string, status string, err error) { + return "some explanation", "COMPLETED", nil +} + func (f *FakeSnykCodeClient) getAutofixResponse(_ context.Context, _ AutofixOptions) (autofixResponse AutofixResponse, status AutofixStatus, err error) { f.AutofixStatus = AutofixStatus{message: completeStatus} return autofixResponse, f.AutofixStatus, nil diff --git a/infrastructure/code/snyk_code_http_client.go b/infrastructure/code/snyk_code_http_client.go index 37d1b7482..8e1aaaa13 100644 --- a/infrastructure/code/snyk_code_http_client.go +++ b/infrastructure/code/snyk_code_http_client.go @@ -524,6 +524,65 @@ func (s *SnykCodeHTTPClient) RunAutofix(ctx context.Context, options AutofixOpti return response, nil } +func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOptions) (ExplainResponse, error) { + requestId, err := performance2.GetTraceId(ctx) + span := s.instrumentor.StartSpan(ctx, "code.RunExplain") + defer span.Finish() + + logger := s.c.Logger().With().Str("method", "code.RunExplain").Str("requestId", requestId).Logger() + if err != nil { + logger.Err(err).Msg(failedToObtainRequestIdString + err.Error()) + return ExplainResponse{}, err + } + logger.Debug().Msg("API: Retrieving explain for bundle") + defer logger.Debug().Msg("API: Retrieving explain done") + + requestBody, err := s.explainRequestBody(&options) + if err != nil { + logger.Err(err).Str("requestBody", string(requestBody)).Msg("error creating request body") + return ExplainResponse{}, err + } + + responseBody, _, err := s.doCall(span.Context(), "POST", "/explain", requestBody) + + if err != nil { + logger.Err(err).Str("responseBody", string(responseBody)).Msg("error response from explain") + return ExplainResponse{}, err + } + + var response ExplainResponse + err = json.Unmarshal(responseBody, &response) + if err != nil { + logger.Err(err).Str("responseBody", string(responseBody)).Msg("error unmarshalling") + return ExplainResponse{}, err + } + return response, nil +} + +func (s *SnykCodeHTTPClient) explainRequestBody(options *ExplainOptions) ([]byte, error) { + _, ruleID, ok := getIssueLangAndRuleId(options.issue) + if !ok { + return nil, errors.New("Issue's ruleID does not follow / format") + } + + request := ExplainRequest{ + Key: ExplainRequestKey{ + Type: "file", + Hash: options.bundleHash, + FilePath: options.filePath, + RuleId: ruleID, + LineNum: options.issue.Range.Start.Line + 1, + }, + AnalysisContext: newCodeRequestContext(), + } + if len(options.shardKey) > 0 { + request.Key.Shard = options.shardKey + } + + requestBody, err := json.Marshal(request) + return requestBody, err +} + func (s *SnykCodeHTTPClient) autofixRequestBody(options *AutofixOptions) ([]byte, error) { _, ruleID, ok := getIssueLangAndRuleId(options.issue) if !ok { diff --git a/infrastructure/code/snyk_code_http_client_interface.go b/infrastructure/code/snyk_code_http_client_interface.go index 82f79563d..2729cf9ac 100644 --- a/infrastructure/code/snyk_code_http_client_interface.go +++ b/infrastructure/code/snyk_code_http_client_interface.go @@ -36,6 +36,13 @@ type AutofixOptions struct { issue snyk.Issue } +type ExplainOptions struct { + bundleHash string + shardKey string + filePath string + issue snyk.Issue +} + type SnykCodeClient interface { GetFilters(ctx context.Context) ( filters FiltersResponse, @@ -81,4 +88,10 @@ type SnykCodeClient interface { status AutofixStatus, err error, ) + + GetAIExplanation(ctx context.Context, baseDir string, options ExplainOptions) ( + explanation string, + status string, + err error, + ) } diff --git a/infrastructure/code/types.go b/infrastructure/code/types.go index 37fa46ecf..3bcf6be11 100644 --- a/infrastructure/code/types.go +++ b/infrastructure/code/types.go @@ -61,6 +61,11 @@ type AutofixResponse struct { AutofixSuggestions []autofixResponseSingleFix `json:"fixes"` } +type ExplainResponse struct { + Status string `json:"status"` + Explanation string `json:"explanation"` +} + type autofixResponseSingleFix struct { Id string `json:"id"` Value string `json:"value"` @@ -77,11 +82,27 @@ type AutofixRequestKey struct { LineNum int `json:"lineNum"` } +type ExplainRequestKey struct { + Type string `json:"type"` + Hash string `json:"hash"` + Shard string `json:"shard"` + FilePath string `json:"filePath"` + RuleId string `json:"ruleId"` + // 1-based to comply with Sarif and Code API, see + // https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html#_Ref493492556 + LineNum int `json:"lineNum"` +} + type AutofixRequest struct { Key AutofixRequestKey `json:"key"` AnalysisContext codeRequestContext `json:"analysisContext"` } +type ExplainRequest struct { + Key ExplainRequestKey `json:"key"` + AnalysisContext codeRequestContext `json:"analysisContext"` +} + // Should implement `error` interface type SnykAutofixFailedError struct { Msg string diff --git a/internal/types/command.go b/internal/types/command.go index 76faf91c0..6ab62a330 100644 --- a/internal/types/command.go +++ b/internal/types/command.go @@ -46,6 +46,7 @@ const ( CodeFixCommand = "snyk.code.fix" CodeSubmitFixFeedback = "snyk.code.submitFixFeedback" CodeFixDiffsCommand = "snyk.code.fixDiffs" + GenerateAIExplanationCommand = "snyk.generateAIExplanation" ) var ( From a90890ad78447747e4a5e22129cc542f11d2dfb3 Mon Sep 17 00:00:00 2001 From: Berkay Berabi Date: Wed, 18 Dec 2024 14:24:19 +0100 Subject: [PATCH 2/9] feat/ai/explain: first working version until deeproxy --- infrastructure/code/snyk_code_http_client.go | 3 ++- infrastructure/code/template/details.html | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/infrastructure/code/snyk_code_http_client.go b/infrastructure/code/snyk_code_http_client.go index 8e1aaaa13..a421f4bb2 100644 --- a/infrastructure/code/snyk_code_http_client.go +++ b/infrastructure/code/snyk_code_http_client.go @@ -534,6 +534,7 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti logger.Err(err).Msg(failedToObtainRequestIdString + err.Error()) return ExplainResponse{}, err } + // we come until here. logger.Debug().Msg("API: Retrieving explain for bundle") defer logger.Debug().Msg("API: Retrieving explain done") @@ -542,7 +543,7 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti logger.Err(err).Str("requestBody", string(requestBody)).Msg("error creating request body") return ExplainResponse{}, err } - + // fails here because deeproxy does not have explain endpoint. so everything works as expected so far responseBody, _, err := s.doCall(span.Context(), "POST", "/explain", requestBody) if err != nil { diff --git a/infrastructure/code/template/details.html b/infrastructure/code/template/details.html index ec807ba8f..005667e7d 100644 --- a/infrastructure/code/template/details.html +++ b/infrastructure/code/template/details.html @@ -300,7 +300,10 @@

- +
+ + +
From fc4452e7de1acae228b71df616d524bceabefd0c Mon Sep 17 00:00:00 2001 From: Berkay Berabi Date: Fri, 20 Dec 2024 10:03:54 +0100 Subject: [PATCH 3/9] feat/ai/explain: added ai explain command and obtained the first successful response from the localhost via portforwarding --- domain/ide/command/generate_ai_explanation.go | 46 +++++++--------- infrastructure/code/ai_explain.go | 35 ++++-------- .../code/fake_snyk_code_api_service.go | 2 +- infrastructure/code/snyk_code_http_client.go | 55 +++++++++++++------ .../code/snyk_code_http_client_interface.go | 9 ++- infrastructure/code/template/details.html | 13 +++++ 6 files changed, 87 insertions(+), 73 deletions(-) diff --git a/domain/ide/command/generate_ai_explanation.go b/domain/ide/command/generate_ai_explanation.go index e29ce7ce0..034d34826 100644 --- a/domain/ide/command/generate_ai_explanation.go +++ b/domain/ide/command/generate_ai_explanation.go @@ -20,17 +20,11 @@ package command import ( "context" "errors" - "path/filepath" - "strings" - - "github.com/sourcegraph/go-lsp" - "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/domain/snyk" "github.com/snyk/snyk-ls/infrastructure/code" "github.com/snyk/snyk-ls/internal/notification" "github.com/snyk/snyk-ls/internal/types" - uri2 "github.com/snyk/snyk-ls/internal/uri" ) @@ -53,40 +47,40 @@ func (cmd *generateAIExplanation) Execute (ctx context.Context) (any, error) { return nil, errors.New("missing required arguments") } - folderURI, ok := args[0].(string) + derivation, ok := args[0].(string) if !ok { - return nil, errors.New("failed to parse folder path") + return nil, errors.New("failed to parse derivation") } - folderPath := uri2.PathFromUri(lsp.DocumentURI(folderURI)) + // folderPath := uri2.PathFromUri(lsp.DocumentURI(folderURI)) - issueURI, ok := args[1].(string) + ruleKey, ok := args[1].(string) if !ok { - return nil, errors.New("failed to parse filepath") + return nil, errors.New("failed to parse ruleKey") } - issuePath := uri2.PathFromUri(lsp.DocumentURI(issueURI)) + // issuePath := uri2.PathFromUri(lsp.DocumentURI(issueURI)) - relPath, err := filepath.Rel(folderPath, issuePath) - if err != nil { - return nil, err - } + // relPath, err := filepath.Rel(folderPath, issuePath) + // if err != nil { + // return nil, err + // } - if strings.HasPrefix(relPath, "..") { - return nil, errors.New("issue path is not within the folder path") - } + // if strings.HasPrefix(relPath, "..") { + // return nil, errors.New("issue path is not within the folder path") + // } - id, ok := args[2].(string) + ruleMessage, ok := args[2].(string) if !ok { - return nil, errors.New("failed to parse issue id") + return nil, errors.New("failed to parse ruleMessage") } - issue := cmd.issueProvider.Issue(id) - if issue.ID == "" { - return nil, errors.New("failed to find issue") - } + // issue := cmd.issueProvider.Issue(id) + // if issue.ID == "" { + // return nil, errors.New("failed to find issue") + // } // Now we need to call cmd.codeScanner.GetAIExplanation - explanation, err := cmd.codeScanner.GetAIExplanation(ctx, folderPath, relPath, issue) + explanation, err := cmd.codeScanner.GetAIExplanation(ctx, derivation, ruleKey, ruleMessage) if err != nil { logger.Err(err).Msgf("received an error from API: %s", err.Error()) return explanation, err diff --git a/infrastructure/code/ai_explain.go b/infrastructure/code/ai_explain.go index 98419abd3..d253fbd93 100644 --- a/infrastructure/code/ai_explain.go +++ b/infrastructure/code/ai_explain.go @@ -19,11 +19,9 @@ package code import ( "context" "errors" - "fmt" "time" "github.com/snyk/snyk-ls/application/config" - "github.com/snyk/snyk-ls/domain/snyk" performance2 "github.com/snyk/snyk-ls/internal/observability/performance" ) @@ -33,9 +31,9 @@ import ( func (sc *Scanner) GetAIExplanation( ctx context.Context, - baseDir string, - filePath string, - issue snyk.Issue, + derivation string, + ruleKey string, + ruleMessage string, ) (explanation string, err error) { method := "GetAIExplanation" logger := config.CurrentConfig().Logger().With().Str("method", method).Logger() @@ -43,24 +41,13 @@ func (sc *Scanner) GetAIExplanation( defer sc.BundleUploader.instrumentor.Finish(span) codeClient := sc.BundleUploader.SnykCode - sc.bundleHashesMutex.RLock() - bundleHash, found := sc.bundleHashes[baseDir] - sc.bundleHashesMutex.RUnlock() - if !found { - return explanation, fmt.Errorf("bundle hash not found for baseDir: %s", baseDir) - } - - encodedNormalizedPath, err := ToEncodedNormalizedPath(baseDir, filePath) - if err != nil { - return explanation, err - } options := ExplainOptions{ - bundleHash: bundleHash, - shardKey: getShardKey(baseDir, config.CurrentConfig().Token()), - filePath: encodedNormalizedPath, - issue: issue, + derivation: derivation, + ruleKey: ruleKey, + ruleMessage: ruleMessage, } + logger.Info().Str("derivation", derivation).Msg("Started retrieving vuln explanation.") ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() @@ -74,20 +61,19 @@ func (sc *Scanner) GetAIExplanation( logger.Error().Msg(msg) return "", errors.New(msg) case <-ticker.C: - explanation, explainStatus, explanationErr := codeClient.GetAIExplanation(span.Context(), baseDir, options) + explanation, explainStatus, explanationErr := codeClient.GetAIExplanation(span.Context(), options) if explanationErr != nil { logger.Err(explanationErr).Msg("Error getting explanation") return "", explanationErr } else if explainStatus == completeStatus { return explanation, nil } - // If err == nil and fixStatus.message != completeStatus, we will keep polling. } } } -func (s *SnykCodeHTTPClient) GetAIExplanation(ctx context.Context, basedir string, options ExplainOptions) ( +func (s *SnykCodeHTTPClient) GetAIExplanation(ctx context.Context, options ExplainOptions) ( explanation string, status string, err error, @@ -103,7 +89,7 @@ func (s *SnykCodeHTTPClient) GetAIExplanation(ctx context.Context, basedir strin if err != nil { return "", status, err } - return explainResponse.Explanation, "COMPLETED", nil + return explainResponse.Explanation, completeStatus, nil } func (s *SnykCodeHTTPClient) getExplainResponse(ctx context.Context, options ExplainOptions) (explainResponse ExplainResponse, err error) { @@ -121,6 +107,7 @@ func (s *SnykCodeHTTPClient) getExplainResponse(ctx context.Context, options Exp logger.Info().Str("requestId", requestId).Msg("Started obtaining explain Response") defer logger.Info().Str("requestId", requestId).Msg("Finished obtaining explain Response") + // we come until here. response, err := s.RunExplain(span.Context(), options) if err != nil { return response, err diff --git a/infrastructure/code/fake_snyk_code_api_service.go b/infrastructure/code/fake_snyk_code_api_service.go index 4efb1ed87..7d1a5e15c 100644 --- a/infrastructure/code/fake_snyk_code_api_service.go +++ b/infrastructure/code/fake_snyk_code_api_service.go @@ -147,7 +147,7 @@ func (f *FakeSnykCodeClient) GetAutofixDiffs(_ context.Context, _ string, _ Auto return f.UnifiedDiffSuggestions, f.AutofixStatus, nil } -func (f *FakeSnykCodeClient) GetAIExplanation(_ context.Context, _ string, _ ExplainOptions) (explanation string, status string, err error) { +func (f *FakeSnykCodeClient) GetAIExplanation(_ context.Context, _ ExplainOptions) (explanation string, status string, err error) { return "some explanation", "COMPLETED", nil } diff --git a/infrastructure/code/snyk_code_http_client.go b/infrastructure/code/snyk_code_http_client.go index a421f4bb2..62b4f3033 100644 --- a/infrastructure/code/snyk_code_http_client.go +++ b/infrastructure/code/snyk_code_http_client.go @@ -17,6 +17,8 @@ package code import ( + "io/ioutil" + "bytes" "context" "encoding/json" @@ -544,41 +546,60 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti return ExplainResponse{}, err } // fails here because deeproxy does not have explain endpoint. so everything works as expected so far - responseBody, _, err := s.doCall(span.Context(), "POST", "/explain", requestBody) + url := "http://localhost:10000/v1/models" + // Make the GET request + resp, err := http.Get(url) + logger.Debug().Msg("API: sent get request") if err != nil { - logger.Err(err).Str("responseBody", string(responseBody)).Msg("error response from explain") - return ExplainResponse{}, err + logger.Err(err).Str("requestBody", string(requestBody)).Msg("error getting response") } + defer resp.Body.Close() + // Read the response body + logger.Debug().Msg("API: before readall") + body, err := ioutil.ReadAll(resp.Body) + logger.Debug().Msg("API: after readall") + if err != nil { + logger.Err(err).Str("requestBody", string(requestBody)).Msg("error reading all response") + } + logger.Debug().Msg("read explain response") + logger.Debug().Str("response body: %s\n", string(body)).Msg("Got the response") - var response ExplainResponse - err = json.Unmarshal(responseBody, &response) + // responseBody, _, err := s.doCall(span.Context(), "POST", "/explain", requestBody) if err != nil { - logger.Err(err).Str("responseBody", string(responseBody)).Msg("error unmarshalling") return ExplainResponse{}, err } + + var response ExplainResponse + response.Status = completeStatus + response.Explanation = "berkay berabi" + // err = json.Unmarshal(responseBody, &response) + // if err != nil { + // logger.Err(err).Str("responseBody", string(responseBody)).Msg("error unmarshalling") + // return ExplainResponse{}, err + // } return response, nil } func (s *SnykCodeHTTPClient) explainRequestBody(options *ExplainOptions) ([]byte, error) { - _, ruleID, ok := getIssueLangAndRuleId(options.issue) - if !ok { - return nil, errors.New("Issue's ruleID does not follow / format") - } + // _, ruleID, ok := getIssueLangAndRuleId(options.issue) + // if !ok { + // return nil, errors.New("Issue's ruleID does not follow / format") + // } request := ExplainRequest{ Key: ExplainRequestKey{ Type: "file", - Hash: options.bundleHash, - FilePath: options.filePath, - RuleId: ruleID, - LineNum: options.issue.Range.Start.Line + 1, + Hash: options.derivation, + FilePath: options.ruleKey, + RuleId: options.ruleKey, + LineNum: 10, }, AnalysisContext: newCodeRequestContext(), } - if len(options.shardKey) > 0 { - request.Key.Shard = options.shardKey - } + // if len(options.shardKey) > 0 { + // request.Key.Shard = options.shardKey + // } requestBody, err := json.Marshal(request) return requestBody, err diff --git a/infrastructure/code/snyk_code_http_client_interface.go b/infrastructure/code/snyk_code_http_client_interface.go index 2729cf9ac..9a0c8970d 100644 --- a/infrastructure/code/snyk_code_http_client_interface.go +++ b/infrastructure/code/snyk_code_http_client_interface.go @@ -37,10 +37,9 @@ type AutofixOptions struct { } type ExplainOptions struct { - bundleHash string - shardKey string - filePath string - issue snyk.Issue + derivation string + ruleKey string + ruleMessage string } type SnykCodeClient interface { @@ -89,7 +88,7 @@ type SnykCodeClient interface { err error, ) - GetAIExplanation(ctx context.Context, baseDir string, options ExplainOptions) ( + GetAIExplanation(ctx context.Context, options ExplainOptions) ( explanation string, status string, err error, diff --git a/infrastructure/code/template/details.html b/infrastructure/code/template/details.html index 005667e7d..6e2bd097e 100644 --- a/infrastructure/code/template/details.html +++ b/infrastructure/code/template/details.html @@ -237,6 +237,19 @@

Fixed Code Examples

+
+

+ DeepCode AI Explanations +

+

⚡ Explain this particular vulnerability using Snyk DeepCode AI

+
+ +
+
+ +
+
+

DeepCode AI Fixes From 037478dfff60392f30ae653a0cf1e28af67f51ab Mon Sep 17 00:00:00 2001 From: Berkay Berabi Date: Tue, 14 Jan 2025 14:39:44 +0100 Subject: [PATCH 4/9] feat/ai/explain: first fully working version of vuln explanation and fix exlanation --- domain/ide/command/generate_ai_explanation.go | 22 ++----- infrastructure/code/ai_explain.go | 2 + infrastructure/code/snyk_code_http_client.go | 63 ++++++++++++++----- .../code/snyk_code_http_client_interface.go | 1 + infrastructure/code/template/details.html | 7 ++- infrastructure/code/types.go | 33 ++++++---- 6 files changed, 83 insertions(+), 45 deletions(-) diff --git a/domain/ide/command/generate_ai_explanation.go b/domain/ide/command/generate_ai_explanation.go index 034d34826..3b941a2f1 100644 --- a/domain/ide/command/generate_ai_explanation.go +++ b/domain/ide/command/generate_ai_explanation.go @@ -51,36 +51,24 @@ func (cmd *generateAIExplanation) Execute (ctx context.Context) (any, error) { if !ok { return nil, errors.New("failed to parse derivation") } - // folderPath := uri2.PathFromUri(lsp.DocumentURI(folderURI)) ruleKey, ok := args[1].(string) if !ok { return nil, errors.New("failed to parse ruleKey") } - // issuePath := uri2.PathFromUri(lsp.DocumentURI(issueURI)) - - // relPath, err := filepath.Rel(folderPath, issuePath) - // if err != nil { - // return nil, err - // } - - // if strings.HasPrefix(relPath, "..") { - // return nil, errors.New("issue path is not within the folder path") - // } - ruleMessage, ok := args[2].(string) if !ok { return nil, errors.New("failed to parse ruleMessage") } - // issue := cmd.issueProvider.Issue(id) - // if issue.ID == "" { - // return nil, errors.New("failed to find issue") - // } + diff, ok := args[3].(string) + if !ok { + return nil, errors.New("failed to parse diff") + } // Now we need to call cmd.codeScanner.GetAIExplanation - explanation, err := cmd.codeScanner.GetAIExplanation(ctx, derivation, ruleKey, ruleMessage) + explanation, err := cmd.codeScanner.GetAIExplanation(ctx, derivation, ruleKey, ruleMessage, diff) if err != nil { logger.Err(err).Msgf("received an error from API: %s", err.Error()) return explanation, err diff --git a/infrastructure/code/ai_explain.go b/infrastructure/code/ai_explain.go index d253fbd93..27fc7e4ad 100644 --- a/infrastructure/code/ai_explain.go +++ b/infrastructure/code/ai_explain.go @@ -34,6 +34,7 @@ func (sc *Scanner) GetAIExplanation( derivation string, ruleKey string, ruleMessage string, + diff string, ) (explanation string, err error) { method := "GetAIExplanation" logger := config.CurrentConfig().Logger().With().Str("method", method).Logger() @@ -46,6 +47,7 @@ func (sc *Scanner) GetAIExplanation( derivation: derivation, ruleKey: ruleKey, ruleMessage: ruleMessage, + diff: diff, } logger.Info().Str("derivation", derivation).Msg("Started retrieving vuln explanation.") diff --git a/infrastructure/code/snyk_code_http_client.go b/infrastructure/code/snyk_code_http_client.go index 62b4f3033..07d255cad 100644 --- a/infrastructure/code/snyk_code_http_client.go +++ b/infrastructure/code/snyk_code_http_client.go @@ -540,16 +540,19 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti logger.Debug().Msg("API: Retrieving explain for bundle") defer logger.Debug().Msg("API: Retrieving explain done") + // construct the requestBody depending on the values given from IDE. requestBody, err := s.explainRequestBody(&options) if err != nil { logger.Err(err).Str("requestBody", string(requestBody)).Msg("error creating request body") return ExplainResponse{}, err } // fails here because deeproxy does not have explain endpoint. so everything works as expected so far - url := "http://localhost:10000/v1/models" + url := "http://localhost:50052/explain" // Make the GET request - resp, err := http.Get(url) + // resp, err := http.Get(url) + logger.Debug().Str("payload body: %s\n", string(requestBody)).Msg("Marshalled payload") + resp, err := http.Post(url, "application/json", bytes.NewBuffer(requestBody)) logger.Debug().Msg("API: sent get request") if err != nil { logger.Err(err).Str("requestBody", string(requestBody)).Msg("error getting response") @@ -570,9 +573,17 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti return ExplainResponse{}, err } + // Variable to hold the parsed data + var data map[string]interface{} + // Unmarshal the JSON string into the map + err = json.Unmarshal(body, &data) + if err != nil { + return ExplainResponse{}, err + } + var response ExplainResponse response.Status = completeStatus - response.Explanation = "berkay berabi" + response.Explanation = data["explanation"].(string) // err = json.Unmarshal(responseBody, &response) // if err != nil { // logger.Err(err).Str("responseBody", string(responseBody)).Msg("error unmarshalling") @@ -586,23 +597,45 @@ func (s *SnykCodeHTTPClient) explainRequestBody(options *ExplainOptions) ([]byte // if !ok { // return nil, errors.New("Issue's ruleID does not follow / format") // } - - request := ExplainRequest{ - Key: ExplainRequestKey{ - Type: "file", - Hash: options.derivation, - FilePath: options.ruleKey, - RuleId: options.ruleKey, - LineNum: 10, - }, - AnalysisContext: newCodeRequestContext(), + logger := s.c.Logger().With().Str("method", "code.explainRequestBody").Logger() + + if options.diff == "" { + request := ExplainRequest{ + VulnExplanation: &ExplainVulnerabilityRequest{ + RuleId: options.ruleKey, + Derivation: options.derivation, + RuleMessage: options.ruleMessage, + ExplanationLength: SHORT, + }, + FixExplanation: nil, + // AnalysisContext: newCodeRequestContext(), + } + requestBody, err := json.Marshal(request) + logger.Debug().Msg("payload for VulnExplanation") + return requestBody, err + } else{ + request := ExplainRequest{ + VulnExplanation: nil, + FixExplanation: &ExplainFixRequest{ + RuleId: options.ruleKey, + // Derivation: options.derivation + // RuleMessage: options.ruleMessage, + Diff: options.diff, + ExplanationLength: SHORT, + }, + // AnalysisContext: newCodeRequestContext(), + } + logger.Debug().Msg("payload for FixExplanation") + requestBody, err := json.Marshal(request) + return requestBody, err } + // if len(options.shardKey) > 0 { // request.Key.Shard = options.shardKey // } - requestBody, err := json.Marshal(request) - return requestBody, err + // requestBody, err := json.Marshal(request) + // return requestBody, err } func (s *SnykCodeHTTPClient) autofixRequestBody(options *AutofixOptions) ([]byte, error) { diff --git a/infrastructure/code/snyk_code_http_client_interface.go b/infrastructure/code/snyk_code_http_client_interface.go index 9a0c8970d..31cacb1d7 100644 --- a/infrastructure/code/snyk_code_http_client_interface.go +++ b/infrastructure/code/snyk_code_http_client_interface.go @@ -40,6 +40,7 @@ type ExplainOptions struct { derivation string ruleKey string ruleMessage string + diff string } type SnykCodeClient interface { diff --git a/infrastructure/code/template/details.html b/infrastructure/code/template/details.html index 6e2bd097e..7145cce85 100644 --- a/infrastructure/code/template/details.html +++ b/infrastructure/code/template/details.html @@ -246,7 +246,7 @@

- +
@@ -315,7 +315,10 @@

- + +
+ +
diff --git a/infrastructure/code/types.go b/infrastructure/code/types.go index 3bcf6be11..032878b01 100644 --- a/infrastructure/code/types.go +++ b/infrastructure/code/types.go @@ -82,15 +82,26 @@ type AutofixRequestKey struct { LineNum int `json:"lineNum"` } -type ExplainRequestKey struct { - Type string `json:"type"` - Hash string `json:"hash"` - Shard string `json:"shard"` - FilePath string `json:"filePath"` - RuleId string `json:"ruleId"` - // 1-based to comply with Sarif and Code API, see - // https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html#_Ref493492556 - LineNum int `json:"lineNum"` +type ExplanationLength string + +const ( + SHORT ExplanationLength = "SHORT" + MEDIUM ExplanationLength = "MEDIUM" + LONG ExplanationLength = "LONG" +) + + +type ExplainVulnerabilityRequest struct { + RuleId string `json:"rule_key"` + RuleMessage string `json:"rule_message"` + Derivation string `json:"derivation"` + ExplanationLength ExplanationLength `json:"explanation_length"` +} + +type ExplainFixRequest struct { + RuleId string `json:"rule_key"` + Diff string `json:"diff"` + ExplanationLength ExplanationLength `json:"explanation_length"` } type AutofixRequest struct { @@ -99,8 +110,8 @@ type AutofixRequest struct { } type ExplainRequest struct { - Key ExplainRequestKey `json:"key"` - AnalysisContext codeRequestContext `json:"analysisContext"` + VulnExplanation *ExplainVulnerabilityRequest `json:"vuln_explanation,omitempty"` + FixExplanation *ExplainFixRequest `json:"fix_explanation,omitempty"` } // Should implement `error` interface From c4bc87c8c101c2dd5ac3a65f431c9643fd0ba2d3 Mon Sep 17 00:00:00 2001 From: Berkay Berabi Date: Wed, 15 Jan 2025 16:45:35 +0100 Subject: [PATCH 5/9] feat/ai/explain: demo fully working --- infrastructure/code/ai_explain.go | 7 ------ infrastructure/code/snyk_code_http_client.go | 23 ++------------------ 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/infrastructure/code/ai_explain.go b/infrastructure/code/ai_explain.go index 27fc7e4ad..dd7b8aac1 100644 --- a/infrastructure/code/ai_explain.go +++ b/infrastructure/code/ai_explain.go @@ -133,10 +133,3 @@ func (s *SnykCodeHTTPClient) getExplainResponse(ctx context.Context, options Exp return response, nil } - - - - - - - diff --git a/infrastructure/code/snyk_code_http_client.go b/infrastructure/code/snyk_code_http_client.go index 07d255cad..ca3a2bdc4 100644 --- a/infrastructure/code/snyk_code_http_client.go +++ b/infrastructure/code/snyk_code_http_client.go @@ -536,7 +536,7 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti logger.Err(err).Msg(failedToObtainRequestIdString + err.Error()) return ExplainResponse{}, err } - // we come until here. + logger.Debug().Msg("API: Retrieving explain for bundle") defer logger.Debug().Msg("API: Retrieving explain done") @@ -546,11 +546,8 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti logger.Err(err).Str("requestBody", string(requestBody)).Msg("error creating request body") return ExplainResponse{}, err } - // fails here because deeproxy does not have explain endpoint. so everything works as expected so far - url := "http://localhost:50052/explain" - // Make the GET request - // resp, err := http.Get(url) + url := "http://localhost:10000/explain" logger.Debug().Str("payload body: %s\n", string(requestBody)).Msg("Marshalled payload") resp, err := http.Post(url, "application/json", bytes.NewBuffer(requestBody)) logger.Debug().Msg("API: sent get request") @@ -568,7 +565,6 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti logger.Debug().Msg("read explain response") logger.Debug().Str("response body: %s\n", string(body)).Msg("Got the response") - // responseBody, _, err := s.doCall(span.Context(), "POST", "/explain", requestBody) if err != nil { return ExplainResponse{}, err } @@ -593,10 +589,6 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti } func (s *SnykCodeHTTPClient) explainRequestBody(options *ExplainOptions) ([]byte, error) { - // _, ruleID, ok := getIssueLangAndRuleId(options.issue) - // if !ok { - // return nil, errors.New("Issue's ruleID does not follow / format") - // } logger := s.c.Logger().With().Str("method", "code.explainRequestBody").Logger() if options.diff == "" { @@ -608,7 +600,6 @@ func (s *SnykCodeHTTPClient) explainRequestBody(options *ExplainOptions) ([]byte ExplanationLength: SHORT, }, FixExplanation: nil, - // AnalysisContext: newCodeRequestContext(), } requestBody, err := json.Marshal(request) logger.Debug().Msg("payload for VulnExplanation") @@ -618,24 +609,14 @@ func (s *SnykCodeHTTPClient) explainRequestBody(options *ExplainOptions) ([]byte VulnExplanation: nil, FixExplanation: &ExplainFixRequest{ RuleId: options.ruleKey, - // Derivation: options.derivation - // RuleMessage: options.ruleMessage, Diff: options.diff, ExplanationLength: SHORT, }, - // AnalysisContext: newCodeRequestContext(), } logger.Debug().Msg("payload for FixExplanation") requestBody, err := json.Marshal(request) return requestBody, err } - - // if len(options.shardKey) > 0 { - // request.Key.Shard = options.shardKey - // } - - // requestBody, err := json.Marshal(request) - // return requestBody, err } func (s *SnykCodeHTTPClient) autofixRequestBody(options *AutofixOptions) ([]byte, error) { From 101fa659cb226e5f4f8a0cf14102a22ee6d5c2eb Mon Sep 17 00:00:00 2001 From: Berkay Berabi Date: Thu, 16 Jan 2025 11:24:32 +0100 Subject: [PATCH 6/9] feat/ai/explain: self review --- README.md | 8 +++--- domain/ide/command/generate_ai_explanation.go | 3 +-- infrastructure/code/ai_explain.go | 8 ++---- .../code/fake_snyk_code_api_service.go | 3 ++- infrastructure/code/snyk_code_http_client.go | 26 +++++-------------- 5 files changed, 16 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 7b7c9779c..92c755668 100644 --- a/README.md +++ b/README.md @@ -384,11 +384,11 @@ Right now the language server supports the following actions: - command: `snyk.generateAIExplanation` - args: - - folderURI string - - fileURI string + - derivation string + - ruleKey string + - ruleMessage string - diff string - - issueID string (UUID) - - returns an array of explanations: + - returns an explanations as string: ```json5 [{ "explainId": "123", diff --git a/domain/ide/command/generate_ai_explanation.go b/domain/ide/command/generate_ai_explanation.go index 3b941a2f1..f97980e9a 100644 --- a/domain/ide/command/generate_ai_explanation.go +++ b/domain/ide/command/generate_ai_explanation.go @@ -43,7 +43,7 @@ func (cmd *generateAIExplanation) Execute (ctx context.Context) (any, error) { logger := config.CurrentConfig().Logger().With().Str("method", "generateAIExplanation.Execute").Logger() args := cmd.command.Arguments - if len(args) < 3 { + if len(args) < 4 { return nil, errors.New("missing required arguments") } @@ -67,7 +67,6 @@ func (cmd *generateAIExplanation) Execute (ctx context.Context) (any, error) { return nil, errors.New("failed to parse diff") } - // Now we need to call cmd.codeScanner.GetAIExplanation explanation, err := cmd.codeScanner.GetAIExplanation(ctx, derivation, ruleKey, ruleMessage, diff) if err != nil { logger.Err(err).Msgf("received an error from API: %s", err.Error()) diff --git a/infrastructure/code/ai_explain.go b/infrastructure/code/ai_explain.go index dd7b8aac1..ae0df86f5 100644 --- a/infrastructure/code/ai_explain.go +++ b/infrastructure/code/ai_explain.go @@ -26,9 +26,6 @@ import ( ) - - - func (sc *Scanner) GetAIExplanation( ctx context.Context, derivation string, @@ -59,13 +56,13 @@ func (sc *Scanner) GetAIExplanation( for { select { case <-timeoutTimer.C: - const msg = "Timeout waiting for explanation." + const msg = "Timeout waiting for an explanation." logger.Error().Msg(msg) return "", errors.New(msg) case <-ticker.C: explanation, explainStatus, explanationErr := codeClient.GetAIExplanation(span.Context(), options) if explanationErr != nil { - logger.Err(explanationErr).Msg("Error getting explanation") + logger.Err(explanationErr).Msg("Error getting an explanation") return "", explanationErr } else if explainStatus == completeStatus { return explanation, nil @@ -109,7 +106,6 @@ func (s *SnykCodeHTTPClient) getExplainResponse(ctx context.Context, options Exp logger.Info().Str("requestId", requestId).Msg("Started obtaining explain Response") defer logger.Info().Str("requestId", requestId).Msg("Finished obtaining explain Response") - // we come until here. response, err := s.RunExplain(span.Context(), options) if err != nil { return response, err diff --git a/infrastructure/code/fake_snyk_code_api_service.go b/infrastructure/code/fake_snyk_code_api_service.go index 7d1a5e15c..2ef435f23 100644 --- a/infrastructure/code/fake_snyk_code_api_service.go +++ b/infrastructure/code/fake_snyk_code_api_service.go @@ -140,6 +140,7 @@ type FakeSnykCodeClient struct { AutofixStatus AutofixStatus Options map[string]AnalysisOptions C *config.Config + Explanation string } func (f *FakeSnykCodeClient) GetAutofixDiffs(_ context.Context, _ string, _ AutofixOptions) (unifiedDiffSuggestions []AutofixUnifiedDiffSuggestion, status AutofixStatus, err error) { @@ -148,7 +149,7 @@ func (f *FakeSnykCodeClient) GetAutofixDiffs(_ context.Context, _ string, _ Auto } func (f *FakeSnykCodeClient) GetAIExplanation(_ context.Context, _ ExplainOptions) (explanation string, status string, err error) { - return "some explanation", "COMPLETED", nil + return f.Explanation, completeStatus, nil } func (f *FakeSnykCodeClient) getAutofixResponse(_ context.Context, _ AutofixOptions) (autofixResponse AutofixResponse, status AutofixStatus, err error) { diff --git a/infrastructure/code/snyk_code_http_client.go b/infrastructure/code/snyk_code_http_client.go index ca3a2bdc4..9852b9c0d 100644 --- a/infrastructure/code/snyk_code_http_client.go +++ b/infrastructure/code/snyk_code_http_client.go @@ -18,7 +18,6 @@ package code import ( "io/ioutil" - "bytes" "context" "encoding/json" @@ -550,19 +549,15 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti url := "http://localhost:10000/explain" logger.Debug().Str("payload body: %s\n", string(requestBody)).Msg("Marshalled payload") resp, err := http.Post(url, "application/json", bytes.NewBuffer(requestBody)) - logger.Debug().Msg("API: sent get request") if err != nil { logger.Err(err).Str("requestBody", string(requestBody)).Msg("error getting response") } defer resp.Body.Close() // Read the response body - logger.Debug().Msg("API: before readall") body, err := ioutil.ReadAll(resp.Body) - logger.Debug().Msg("API: after readall") if err != nil { logger.Err(err).Str("requestBody", string(requestBody)).Msg("error reading all response") } - logger.Debug().Msg("read explain response") logger.Debug().Str("response body: %s\n", string(body)).Msg("Got the response") if err != nil { @@ -591,32 +586,25 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti func (s *SnykCodeHTTPClient) explainRequestBody(options *ExplainOptions) ([]byte, error) { logger := s.c.Logger().With().Str("method", "code.explainRequestBody").Logger() + var request ExplainRequest if options.diff == "" { - request := ExplainRequest{ - VulnExplanation: &ExplainVulnerabilityRequest{ + request.VulnExplanation = &ExplainVulnerabilityRequest{ RuleId: options.ruleKey, Derivation: options.derivation, RuleMessage: options.ruleMessage, ExplanationLength: SHORT, - }, - FixExplanation: nil, - } - requestBody, err := json.Marshal(request) + } logger.Debug().Msg("payload for VulnExplanation") - return requestBody, err } else{ - request := ExplainRequest{ - VulnExplanation: nil, - FixExplanation: &ExplainFixRequest{ + request.FixExplanation = &ExplainFixRequest{ RuleId: options.ruleKey, Diff: options.diff, ExplanationLength: SHORT, - }, - } + } logger.Debug().Msg("payload for FixExplanation") - requestBody, err := json.Marshal(request) - return requestBody, err } + requestBody, err := json.Marshal(request) + return requestBody, err } func (s *SnykCodeHTTPClient) autofixRequestBody(options *AutofixOptions) ([]byte, error) { From 87cefdf629c8d7bc53b220e8ab5c6d038e2b509a Mon Sep 17 00:00:00 2001 From: Berkay Berabi Date: Thu, 16 Jan 2025 11:40:04 +0100 Subject: [PATCH 7/9] feat/ai/explain: remove redundant unmarshaling --- README.md | 1 - infrastructure/code/snyk_code_http_client.go | 22 +++++++------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 92c755668..74918d7d9 100644 --- a/README.md +++ b/README.md @@ -383,7 +383,6 @@ Right now the language server supports the following actions: - `Generate AI Explanation` allows to retrieve explainations for various use-cases. - command: `snyk.generateAIExplanation` - args: - - derivation string - ruleKey string - ruleMessage string diff --git a/infrastructure/code/snyk_code_http_client.go b/infrastructure/code/snyk_code_http_client.go index 9852b9c0d..a240d1d8c 100644 --- a/infrastructure/code/snyk_code_http_client.go +++ b/infrastructure/code/snyk_code_http_client.go @@ -553,33 +553,25 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti logger.Err(err).Str("requestBody", string(requestBody)).Msg("error getting response") } defer resp.Body.Close() + // Read the response body - body, err := ioutil.ReadAll(resp.Body) + responseBody, err := ioutil.ReadAll(resp.Body) if err != nil { logger.Err(err).Str("requestBody", string(requestBody)).Msg("error reading all response") } - logger.Debug().Str("response body: %s\n", string(body)).Msg("Got the response") + logger.Debug().Str("response body: %s\n", string(responseBody)).Msg("Got the response") if err != nil { return ExplainResponse{}, err } - // Variable to hold the parsed data - var data map[string]interface{} - // Unmarshal the JSON string into the map - err = json.Unmarshal(body, &data) + var response ExplainResponse + response.Status = completeStatus + err = json.Unmarshal(responseBody, &response) if err != nil { + logger.Err(err).Str("responseBody", string(responseBody)).Msg("error unmarshalling") return ExplainResponse{}, err } - - var response ExplainResponse - response.Status = completeStatus - response.Explanation = data["explanation"].(string) - // err = json.Unmarshal(responseBody, &response) - // if err != nil { - // logger.Err(err).Str("responseBody", string(responseBody)).Msg("error unmarshalling") - // return ExplainResponse{}, err - // } return response, nil } From 743f8b9c15f40ff120be93d21980085ed70c35fc Mon Sep 17 00:00:00 2001 From: Berkay Berabi Date: Thu, 16 Jan 2025 12:05:04 +0100 Subject: [PATCH 8/9] fix/ai/explain: fix mispell --- infrastructure/code/snyk_code_http_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/code/snyk_code_http_client.go b/infrastructure/code/snyk_code_http_client.go index 9c2b99517..fb15441dc 100644 --- a/infrastructure/code/snyk_code_http_client.go +++ b/infrastructure/code/snyk_code_http_client.go @@ -547,7 +547,7 @@ func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOpti } url := "http://localhost:10000/explain" - logger.Debug().Str("payload body: %s\n", string(requestBody)).Msg("Marshalled payload") + logger.Debug().Str("payload body: %s\n", string(requestBody)).Msg("Marshaled payload") resp, err := http.Post(url, "application/json", bytes.NewBuffer(requestBody)) if err != nil { logger.Err(err).Str("requestBody", string(requestBody)).Msg("error getting response") From 894e40aca1710ea9a2848df52ba3aae06ae44e3b Mon Sep 17 00:00:00 2001 From: Berkay Berabi Date: Thu, 16 Jan 2025 12:23:50 +0100 Subject: [PATCH 9/9] fix/ai/explain: fix import grouping --- infrastructure/code/snyk_code_http_client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/infrastructure/code/snyk_code_http_client.go b/infrastructure/code/snyk_code_http_client.go index fb15441dc..ed3e31bdf 100644 --- a/infrastructure/code/snyk_code_http_client.go +++ b/infrastructure/code/snyk_code_http_client.go @@ -18,6 +18,7 @@ package code import ( "io/ioutil" + "bytes" "context" "encoding/json"