diff --git a/internal/commands/result.go b/internal/commands/result.go index dac7abb9b..585e8f384 100644 --- a/internal/commands/result.go +++ b/internal/commands/result.go @@ -1662,7 +1662,7 @@ func exportGlSastResults(targetFile string, results *wrappers.ScanResultsCollect if err != nil { return errors.Wrapf(err, "%s: failed to add scan to gl-sast report", failedListingResults) } - convertCxResultToGlSastVulnerability(results, glSast, summary.BaseURI) + convertCxResultToGlSastVulnerability(results, glSast, summary) resultsJSON, err := json.Marshal(glSast) if err != nil { return errors.Wrapf(err, "%s: failed to serialize gl-sast report ", failedListingResults) @@ -2031,10 +2031,10 @@ func convertCxResultsToSarif(results *wrappers.ScanResultsCollection) *wrappers. return sarif } -func convertCxResultToGlSastVulnerability(results *wrappers.ScanResultsCollection, glSast *wrappers.GlSastResultsCollection, summaryBaseURI string) { +func convertCxResultToGlSastVulnerability(results *wrappers.ScanResultsCollection, glSast *wrappers.GlSastResultsCollection, summary *wrappers.ResultSummary) { for _, result := range results.Results { if strings.TrimSpace(result.Type) == commonParams.SastType { - glSast = parseGlSastVulnerability(result, glSast, summaryBaseURI) + glSast = parseGlSastVulnerability(result, glSast, summary) } } } @@ -2054,7 +2054,9 @@ func convertCxResultToGlScaFiles(results *wrappers.ScanResultsCollection, glScaR } } } -func parseGlSastVulnerability(result *wrappers.ScanResult, glSast *wrappers.GlSastResultsCollection, summaryBaseURI string) *wrappers.GlSastResultsCollection { +func parseGlSastVulnerability(result *wrappers.ScanResult, glSast *wrappers.GlSastResultsCollection, summary *wrappers.ResultSummary) *wrappers.GlSastResultsCollection { + hostName := parseURI(summary.BaseURI) + queryName := result.ScanResultData.QueryName fileName := result.ScanResultData.Nodes[0].FileName lineNumber := strconv.FormatUint(uint64(result.ScanResultData.Nodes[0].Line), 10) @@ -2063,13 +2065,14 @@ func parseGlSastVulnerability(result *wrappers.ScanResult, glSast *wrappers.GlSa ID := fmt.Sprintf("%s:%s:%s", queryName, fileName, lineNumber) category := fmt.Sprintf("%s-%s", wrappers.VendorName, result.Type) message := fmt.Sprintf("%s@%s:%s", queryName, fileName, lineNumber) + QueryDescriptionLink := fmt.Sprintf("%s/results/%s/%s/sast/description/%s/%s", hostName, summary.ScanID, summary.ProjectID, result.VulnerabilityDetails.CweID, result.ScanResultData.QueryID) glSast.Vulnerabilities = append(glSast.Vulnerabilities, wrappers.GlVulnerabilities{ ID: ID, Category: category, Name: queryName, Message: message, - Description: result.Description, + Description: result.Description + " \n" + QueryDescriptionLink, CVE: ID, Severity: cases.Title(language.English).String(result.Severity), Confidence: cases.Title(language.English).String(result.Severity), @@ -2083,7 +2086,7 @@ func parseGlSastVulnerability(result *wrappers.ScanResult, glSast *wrappers.GlSa { Type: "cxOneScan", Name: "CxOne Scan", - URL: summaryBaseURI, + URL: summary.BaseURI, Value: result.ID, }, }, @@ -2889,6 +2892,16 @@ type ScannerResponse struct { ErrorCode string `json:"ErrorCode,omitempty"` } +func parseURI(summaryBaseURI string) (hostName string) { + parsedURL, err := url.Parse(summaryBaseURI) + if err != nil { + return "" + } + hostName = fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host) + + return hostName +} + func printWarningIfIgnorePolicyOmiited() { fmt.Printf("\n Warning: The --ignore-policy flag was not implemented because you don’t have the required permission.\n Only users with 'override-policy-management' permission can use this flag. \n\n") } diff --git a/internal/commands/result_test.go b/internal/commands/result_test.go index e60a35d14..5558e7153 100644 --- a/internal/commands/result_test.go +++ b/internal/commands/result_test.go @@ -1095,7 +1095,7 @@ func TestRunGetResultsByScanIdSummaryConsoleFormat_ScsCompleted_ScsCompletedInRe "Expected SCS summary:"+scsSummary) secretDetectionSummary := secretDetectionLine assert.Equal(t, strings.Contains(cleanString, secretDetectionSummary), true, - "Expected Secret Detection summary:"+secretDetectionSummary) + "Expected Secret Detection summary:"+secretDetectionLine) scorecardSummary := "| Scorecard 0 0 0 1 0 Completed |" assert.Equal(t, strings.Contains(cleanString, scorecardSummary), true, "Expected Scorecard summary:"+scorecardSummary) @@ -1130,7 +1130,7 @@ func TestRunGetResultsByScanIdSummaryConsoleFormat_ScsPartial_ScsPartialInReport "Expected SCS summary:"+scsSummary) secretDetectionSummary := secretDetectionLine assert.Equal(t, strings.Contains(cleanString, secretDetectionSummary), true, - "Expected Secret Detection summary:"+secretDetectionSummary) + "Expected Secret Detection summary:"+secretDetectionLine) scorecardSummary := " | Scorecard 0 0 0 0 0 Failed |" assert.Equal(t, strings.Contains(cleanString, scorecardSummary), true, "Expected Scorecard summary:"+scorecardSummary) @@ -1157,7 +1157,7 @@ func TestRunGetResultsByScanIdSummaryConsoleFormat_ScsScorecardNotScanned_Scorec "Expected SCS summary:"+scsSummary) secretDetectionSummary := secretDetectionLine assert.Equal(t, strings.Contains(stdoutString, secretDetectionSummary), true, - "Expected Secret Detection summary:"+secretDetectionSummary) + "Expected Secret Detection summary:"+secretDetectionLine) scorecardSummary := "| Scorecard - - - - - - |" assert.Equal(t, strings.Contains(stdoutString, scorecardSummary), true, "Expected Scorecard summary:"+scorecardSummary) @@ -1697,3 +1697,77 @@ func TestIgnorePolicyWithPermission(t *testing.T) { output := buf.String() assert.Assert(t, !strings.Contains(output, "Warning: The --ignore-policy flag was not implemented because you don’t have the required permission."), "'Ignore Policy flag omitted because you dont have permission' should not be present in the output") } + +func TestParseGlSastVulnerability_QueryDescriptionLink_Succeed(t *testing.T) { + mockResult := createMockScanResult("q1234", "c5678") + glSast := &wrappers.GlSastResultsCollection{} + summary := &wrappers.ResultSummary{ + BaseURI: "https://example.com/overview", + ScanID: "scanID", + ProjectID: "projectID", + } + expectedURL := "https://example.com/results/scanID/projectID/sast/description/c5678/q1234" + + glSast = parseGlSastVulnerability(mockResult, glSast, summary) + + assert.Assert(t, len(glSast.Vulnerabilities) > 0) + + actualURL := extractURLFromDescription(glSast.Vulnerabilities[0].Description) + + assert.Equal(t, actualURL, expectedURL, "QueryDescriptionLink URL does not match expected format") +} + +func TestParseGlSastVulnerability_QueryDescriptionLink_Negative(t *testing.T) { + mockResult := createMockScanResult("", "") + glSast := &wrappers.GlSastResultsCollection{} + summary := &wrappers.ResultSummary{ + BaseURI: "invalid-url", + ScanID: "scanID", + ProjectID: "projectID", + } + expectedPattern := "/results/scanID/projectID/sast/description//" + + glSast = parseGlSastVulnerability(mockResult, glSast, summary) + + assert.Assert(t, len(glSast.Vulnerabilities) > 0) + vuln := glSast.Vulnerabilities[0] + + assert.Assert(t, strings.Contains(vuln.Description, expectedPattern), + "URL should contain pattern with empty values") + + actualURL := extractURLFromDescription(vuln.Description) + assert.Assert(t, actualURL != "", "Extracted URL should not be empty") +} + +func createMockScanResult(queryID, cweID string) *wrappers.ScanResult { + return &wrappers.ScanResult{ + Type: "sast", + ScanResultData: wrappers.ScanResultData{ + QueryName: "TestQuery", + QueryID: queryID, + Nodes: []*wrappers.ScanResultNode{ + { + FileName: "file.go", + Line: 42, + Length: 1, + }, + }, + }, + VulnerabilityDetails: wrappers.VulnerabilityDetails{ + CweID: cweID, + }, + ID: "vuln-1", + Description: "desc-", + Severity: "high", + } +} + +func extractURLFromDescription(description string) string { + parts := strings.Split(description, "http") + if len(parts) == 1 { + return "http" + strings.Split(parts[0], " ")[0] + } else if len(parts) > 1 { + return "http" + strings.Split(parts[1], " ")[0] + } + return "" +} diff --git a/internal/wrappers/results-gl-sast.go b/internal/wrappers/results-gl-sast.go index 5b1928df9..9d00b4fe0 100644 --- a/internal/wrappers/results-gl-sast.go +++ b/internal/wrappers/results-gl-sast.go @@ -6,7 +6,7 @@ const ( AnalyzerURL = "https://checkmarx.com/" VendorName = "Checkmarx" SastSchema = "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json" - SastSchemaVersion = "15.0" + SastSchemaVersion = "15.0.0" ) type GlSastResultsCollection struct { diff --git a/test/integration/result_test.go b/test/integration/result_test.go index 593f415f5..ce09dca46 100644 --- a/test/integration/result_test.go +++ b/test/integration/result_test.go @@ -725,3 +725,55 @@ func TestResultsIncludePublishedAtJsonOutput(t *testing.T) { } } } + +func TestCreateQueryDescriptionLinkInGlSASTReport(t *testing.T) { + scanID, _ := getRootScan(t) + _ = executeCmdNilAssertion( + t, "Results show generating gl-sast report should pass", + "results", "show", + flag(params.ScanIDFlag), scanID, + flag(params.TargetFormatFlag), printer.FormatGLSast, + flag(params.TargetPathFlag), resultsDirectory, + flag(params.TargetFlag), fileName, + ) + + defer os.RemoveAll(resultsDirectory) + reportPath := fmt.Sprintf("%s%s.%s-%s", resultsDirectory, fileName, printer.FormatGLSast, fileExtention) + file, err := os.ReadFile(reportPath) + assert.NilError(t, err, "error reading gl-sast file") + + var glReport wrappers.GlSastResultsCollection + err = json.Unmarshal(file, &glReport) + assert.NilError(t, err, "error unmarshalling gl-sast file") + + link, found := findQueryDescriptionLink(glReport) + assert.Assert(t, found, "Should find at least one QueryDescriptionLink") + + t.Logf("Found QueryDescriptionLink: %s", link) +} + +func findQueryDescriptionLink(glReport wrappers.GlSastResultsCollection) (string, bool) { + for _, vulnerability := range glReport.Vulnerabilities { + if !strings.Contains(vulnerability.Description, "/results/") { + continue + } + + if httpIndex := strings.Index(vulnerability.Description, "http"); httpIndex != -1 { + urlStart := httpIndex + urlEnd := len(vulnerability.Description) + + for _, terminator := range []string{" ", "\n", "\t", ")", "]", "}"} { + if idx := strings.Index(vulnerability.Description[urlStart:], terminator); idx != -1 { + urlEnd = urlStart + idx + break + } + } + + link := vulnerability.Description[urlStart:urlEnd] + if link != "" { + return link, true + } + } + } + return "", false +}