diff --git a/README.md b/README.md index fcebb3ed2..74918d7d9 100644 --- a/README.md +++ b/README.md @@ -380,6 +380,20 @@ 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: + - derivation string + - ruleKey string + - ruleMessage string + - diff string + - returns an explanations as string: + ```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 +407,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..f97980e9a --- /dev/null +++ b/domain/ide/command/generate_ai_explanation.go @@ -0,0 +1,76 @@ +/* + * © 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" + "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" +) + + +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) < 4 { + return nil, errors.New("missing required arguments") + } + + derivation, ok := args[0].(string) + if !ok { + return nil, errors.New("failed to parse derivation") + } + + ruleKey, ok := args[1].(string) + if !ok { + return nil, errors.New("failed to parse ruleKey") + } + + ruleMessage, ok := args[2].(string) + if !ok { + return nil, errors.New("failed to parse ruleMessage") + } + + diff, ok := args[3].(string) + if !ok { + return nil, errors.New("failed to parse diff") + } + + 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 + } + return explanation, nil +} diff --git a/infrastructure/code/ai_explain.go b/infrastructure/code/ai_explain.go new file mode 100644 index 000000000..ae0df86f5 --- /dev/null +++ b/infrastructure/code/ai_explain.go @@ -0,0 +1,131 @@ +/* + * © 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" + "time" + + "github.com/snyk/snyk-ls/application/config" + performance2 "github.com/snyk/snyk-ls/internal/observability/performance" +) + + +func (sc *Scanner) GetAIExplanation( + ctx context.Context, + derivation string, + ruleKey string, + ruleMessage string, + diff string, +) (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 + + options := ExplainOptions{ + derivation: derivation, + ruleKey: ruleKey, + ruleMessage: ruleMessage, + diff: diff, + } + logger.Info().Str("derivation", derivation).Msg("Started retrieving vuln explanation.") + + 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 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 an explanation") + return "", explanationErr + } else if explainStatus == completeStatus { + return explanation, nil + } + } + } +} + + +func (s *SnykCodeHTTPClient) GetAIExplanation(ctx context.Context, 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, completeStatus, 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 d02fe88ca..82d448fb5 100644 --- a/infrastructure/code/fake_snyk_code_api_service.go +++ b/infrastructure/code/fake_snyk_code_api_service.go @@ -141,6 +141,7 @@ type FakeSnykCodeClient struct { Options map[string]AnalysisOptions C *config.Config FeedbackSent string + Explanation string } func (f *FakeSnykCodeClient) GetAutofixDiffs(_ context.Context, _ string, _ AutofixOptions) (unifiedDiffSuggestions []AutofixUnifiedDiffSuggestion, status AutofixStatus, err error) { @@ -148,6 +149,10 @@ func (f *FakeSnykCodeClient) GetAutofixDiffs(_ context.Context, _ string, _ Auto return f.UnifiedDiffSuggestions, f.AutofixStatus, nil } +func (f *FakeSnykCodeClient) GetAIExplanation(_ context.Context, _ ExplainOptions) (explanation string, status string, err error) { + return f.Explanation, completeStatus, 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 d11f3af4c..ed3e31bdf 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" @@ -524,6 +526,80 @@ 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") + + // 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 + } + + url := "http://localhost:10000/explain" + 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") + } + defer resp.Body.Close() + + // Read the response 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(responseBody)).Msg("Got the response") + + if err != nil { + return ExplainResponse{}, err + } + + 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 + } + return response, nil +} + +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.VulnExplanation = &ExplainVulnerabilityRequest{ + RuleId: options.ruleKey, + Derivation: options.derivation, + RuleMessage: options.ruleMessage, + ExplanationLength: SHORT, + } + logger.Debug().Msg("payload for VulnExplanation") + } else{ + 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 +} + 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..31cacb1d7 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 { + derivation string + ruleKey string + ruleMessage string + diff string +} + type SnykCodeClient interface { GetFilters(ctx context.Context) ( filters FiltersResponse, @@ -81,4 +88,10 @@ type SnykCodeClient interface { status AutofixStatus, err error, ) + + 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 ec807ba8f..7145cce85 100644 --- a/infrastructure/code/template/details.html +++ b/infrastructure/code/template/details.html @@ -237,6 +237,19 @@
⚡ Explain this particular vulnerability using Snyk DeepCode AI
+