diff --git a/artifactory/utils/dependenciesutils.go b/artifactory/utils/dependenciesutils.go index b65755bcc..950508647 100644 --- a/artifactory/utils/dependenciesutils.go +++ b/artifactory/utils/dependenciesutils.go @@ -3,11 +3,6 @@ package utils import ( "errors" "fmt" - "net/http" - "os" - "path" - "path/filepath" - "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" @@ -17,6 +12,10 @@ import ( "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/io/httputils" "github.com/jfrog/jfrog-client-go/utils/log" + "net/http" + "os" + "path" + "path/filepath" ) const ( diff --git a/xray/commands/utils/utils_test.go b/xray/commands/utils/utils_test.go index 88487da59..de26cb7ba 100644 --- a/xray/commands/utils/utils_test.go +++ b/xray/commands/utils/utils_test.go @@ -72,7 +72,7 @@ func TestFilterResultIfNeeded(t *testing.T) { }, }, params: ScanGraphParams{ - severityLevel: 3, + severityLevel: 8, }, expected: services.ScanResponse{ Violations: []services.Violation{ diff --git a/xray/utils/resultstable.go b/xray/utils/resultstable.go index 2be8e8c8c..39af61f15 100644 --- a/xray/utils/resultstable.go +++ b/xray/utils/resultstable.go @@ -153,14 +153,7 @@ func prepareViolations(violations []services.Violation, extendedResults *Extende } // Sort the rows by severity and whether the row contains fixed versions - sort.Slice(securityViolationsRows, func(i, j int) bool { - if securityViolationsRows[i].SeverityNumValue != securityViolationsRows[j].SeverityNumValue { - return securityViolationsRows[i].SeverityNumValue > securityViolationsRows[j].SeverityNumValue - } else if securityViolationsRows[i].Applicable != securityViolationsRows[j].Applicable { - return sortByApplicableValue(i, j, securityViolationsRows) - } - return len(securityViolationsRows[i].FixedVersions) > 0 && len(securityViolationsRows[j].FixedVersions) > 0 - }) + sortVulnerabilityOrViolationRows(securityViolationsRows) sort.Slice(licenseViolationsRows, func(i, j int) bool { return licenseViolationsRows[i].SeverityNumValue > licenseViolationsRows[j].SeverityNumValue }) @@ -231,15 +224,17 @@ func prepareVulnerabilities(vulnerabilities []services.Vulnerability, extendedRe } } - sort.Slice(vulnerabilitiesRows, func(i, j int) bool { - if vulnerabilitiesRows[i].SeverityNumValue != vulnerabilitiesRows[j].SeverityNumValue { - return vulnerabilitiesRows[i].SeverityNumValue > vulnerabilitiesRows[j].SeverityNumValue - } else if vulnerabilitiesRows[i].Applicable != vulnerabilitiesRows[j].Applicable { - sortByApplicableValue(i, j, vulnerabilitiesRows) + sortVulnerabilityOrViolationRows(vulnerabilitiesRows) + return vulnerabilitiesRows, nil +} + +func sortVulnerabilityOrViolationRows(rows []formats.VulnerabilityOrViolationRow) { + sort.Slice(rows, func(i, j int) bool { + if rows[i].SeverityNumValue != rows[j].SeverityNumValue { + return rows[i].SeverityNumValue > rows[j].SeverityNumValue } - return len(vulnerabilitiesRows[i].FixedVersions) > 0 && len(vulnerabilitiesRows[j].FixedVersions) > 0 + return len(rows[i].FixedVersions) > 0 && len(rows[j].FixedVersions) > 0 }) - return vulnerabilitiesRows, nil } // PrintLicensesTable prints the licenses in a table. @@ -350,8 +345,8 @@ func prepareIacs(iacs []IacOrSecretResult, isTable bool) []formats.IacSecretsRow func PrintIacTable(iacs []IacOrSecretResult, entitledForIacScan bool) error { if entitledForIacScan { iacRows := prepareIacs(iacs, true) - return coreutils.PrintTable(formats.ConvertToIacTableRow(iacRows), "Iac Violations", - "✨ No Iac violations were found ✨", false) + return coreutils.PrintTable(formats.ConvertToIacTableRow(iacRows), "Infrastructure as Code Vulnerabilities", + "✨ No Infrastructure as Code vulnerabilities were found ✨", false) } return nil } @@ -534,20 +529,24 @@ func (s *Severity) printableTitle(isTable bool) string { var Severities = map[string]map[string]*Severity{ "Critical": { - ApplicableStringValue: {emoji: "💀", title: "Critical", numValue: 4, style: color.New(color.BgLightRed, color.LightWhite)}, - NotApplicableStringValue: {emoji: "👌", title: "Critical", numValue: 4}, + ApplicableStringValue: {emoji: "💀", title: "Critical", numValue: 12, style: color.New(color.BgLightRed, color.LightWhite)}, + ApplicabilityUndeterminedStringValue: {emoji: "💀", title: "Critical", numValue: 11, style: color.New(color.BgLightRed, color.LightWhite)}, + NotApplicableStringValue: {emoji: "👌", title: "Critical", numValue: 10}, }, "High": { - ApplicableStringValue: {emoji: "🔥", title: "High", numValue: 3, style: color.New(color.Red)}, - NotApplicableStringValue: {emoji: "👌", title: "High", numValue: 3}, + ApplicableStringValue: {emoji: "🔥", title: "High", numValue: 9, style: color.New(color.Red)}, + ApplicabilityUndeterminedStringValue: {emoji: "🔥", title: "High", numValue: 8, style: color.New(color.Red)}, + NotApplicableStringValue: {emoji: "👌", title: "High", numValue: 7}, }, "Medium": { - ApplicableStringValue: {emoji: "🎃", title: "Medium", numValue: 2, style: color.New(color.Yellow)}, - NotApplicableStringValue: {emoji: "👌", title: "Medium", numValue: 2}, + ApplicableStringValue: {emoji: "🎃", title: "Medium", numValue: 6, style: color.New(color.Yellow)}, + ApplicabilityUndeterminedStringValue: {emoji: "🎃", title: "Medium", numValue: 5, style: color.New(color.Yellow)}, + NotApplicableStringValue: {emoji: "👌", title: "Medium", numValue: 4}, }, "Low": { - ApplicableStringValue: {emoji: "👻", title: "Low", numValue: 1}, - NotApplicableStringValue: {emoji: "👌", title: "Low", numValue: 1}, + ApplicableStringValue: {emoji: "👻", title: "Low", numValue: 3}, + ApplicabilityUndeterminedStringValue: {emoji: "👻", title: "Low", numValue: 2}, + NotApplicableStringValue: {emoji: "👌", title: "Low", numValue: 1}, }, } @@ -572,10 +571,15 @@ func GetSeverity(severityTitle string, applicable string) *Severity { if Severities[severityTitle] == nil { return &Severity{title: severityTitle} } - if applicable == NotApplicableStringValue { + + switch applicable { + case NotApplicableStringValue: return Severities[severityTitle][NotApplicableStringValue] + case ApplicableStringValue: + return Severities[severityTitle][ApplicableStringValue] + default: + return Severities[severityTitle][ApplicabilityUndeterminedStringValue] } - return Severities[severityTitle][ApplicableStringValue] } type operationalRiskViolationReadableData struct { @@ -825,15 +829,6 @@ func getApplicableCveValue(extendedResults *ExtendedScanResults, xrayCves []form return ApplicabilityUndeterminedStringValue } -func getApplicableCveNumValue(stringValue string) int { - if stringValue == ApplicableStringValue { - return 3 - } else if stringValue == ApplicabilityUndeterminedStringValue { - return 2 - } - return 1 -} - func printApplicableCveValue(applicableValue string, isTable bool) string { if applicableValue == ApplicableStringValue && isTable && (log.IsStdOutTerminal() && log.IsColorsSupported() || os.Getenv("GITLAB_CI") != "") { @@ -841,8 +836,3 @@ func printApplicableCveValue(applicableValue string, isTable bool) string { } return applicableValue } - -func sortByApplicableValue(i int, j int, securityViolationsRows []formats.VulnerabilityOrViolationRow) bool { - return getApplicableCveNumValue(securityViolationsRows[i].Applicable) > - getApplicableCveNumValue(securityViolationsRows[j].Applicable) -} diff --git a/xray/utils/resultstable_test.go b/xray/utils/resultstable_test.go index cc16a108e..ec260c1d6 100644 --- a/xray/utils/resultstable_test.go +++ b/xray/utils/resultstable_test.go @@ -466,6 +466,108 @@ func TestGetApplicableCveValue(t *testing.T) { } } +func TestSortVulnerabilityOrViolationRows(t *testing.T) { + testCases := []struct { + name string + rows []formats.VulnerabilityOrViolationRow + expectedOrder []string + }{ + { + name: "Sort by severity with different severity values", + rows: []formats.VulnerabilityOrViolationRow{ + { + Summary: "Summary 1", + Severity: "High", + SeverityNumValue: 9, + FixedVersions: []string{}, + ImpactedDependencyName: "Dependency 1", + ImpactedDependencyVersion: "1.0.0", + }, + { + Summary: "Summary 2", + Severity: "Critical", + SeverityNumValue: 12, + FixedVersions: []string{"1.0.0"}, + ImpactedDependencyName: "Dependency 2", + ImpactedDependencyVersion: "2.0.0", + }, + { + Summary: "Summary 3", + Severity: "Medium", + SeverityNumValue: 6, + FixedVersions: []string{}, + ImpactedDependencyName: "Dependency 3", + ImpactedDependencyVersion: "3.0.0", + }, + }, + expectedOrder: []string{"Dependency 2", "Dependency 1", "Dependency 3"}, + }, + { + name: "Sort by severity with same severity values, but different fixed versions", + rows: []formats.VulnerabilityOrViolationRow{ + { + Summary: "Summary 1", + Severity: "Critical", + SeverityNumValue: 12, + FixedVersions: []string{"1.0.0"}, + ImpactedDependencyName: "Dependency 1", + ImpactedDependencyVersion: "1.0.0", + }, + { + Summary: "Summary 2", + Severity: "Critical", + SeverityNumValue: 12, + FixedVersions: []string{}, + ImpactedDependencyName: "Dependency 2", + ImpactedDependencyVersion: "2.0.0", + }, + }, + expectedOrder: []string{"Dependency 1", "Dependency 2"}, + }, + { + name: "Sort by severity with same severity values different applicability", + rows: []formats.VulnerabilityOrViolationRow{ + { + Summary: "Summary 1", + Severity: "Critical", + Applicable: ApplicableStringValue, + SeverityNumValue: 13, + FixedVersions: []string{"1.0.0"}, + ImpactedDependencyName: "Dependency 1", + ImpactedDependencyVersion: "1.0.0", + }, + { + Summary: "Summary 2", + Applicable: NotApplicableStringValue, + Severity: "Critical", + SeverityNumValue: 11, + ImpactedDependencyName: "Dependency 2", + ImpactedDependencyVersion: "2.0.0", + }, + { + Summary: "Summary 3", + Applicable: ApplicabilityUndeterminedStringValue, + Severity: "Critical", + SeverityNumValue: 12, + ImpactedDependencyName: "Dependency 3", + ImpactedDependencyVersion: "2.0.0", + }, + }, + expectedOrder: []string{"Dependency 1", "Dependency 3", "Dependency 2"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sortVulnerabilityOrViolationRows(tc.rows) + + for i, row := range tc.rows { + assert.Equal(t, tc.expectedOrder[i], row.ImpactedDependencyName) + } + }) + } +} + func newBoolPtr(v bool) *bool { return &v } diff --git a/xray/utils/resultwriter.go b/xray/utils/resultwriter.go index f1d8080be..fb775bd33 100644 --- a/xray/utils/resultwriter.go +++ b/xray/utils/resultwriter.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "os" "strconv" "strings" @@ -35,10 +36,16 @@ var OutputFormats = []string{string(Table), string(Json), string(SimpleJson), st var CurationOutputFormats = []string{string(Table), string(Json)} type sarifProperties struct { - Cves string - Headline string - Severity string - Description string + Applicable string + Cves string + Headline string + Severity string + Description string + MarkdownDescription string + XrayID string + File string + LineColumn string + SecretsOrIacType string } // PrintScanResults prints the scan results in the specified format. @@ -66,7 +73,7 @@ func PrintScanResults(results *ExtendedScanResults, simpleJsonError []formats.Si case Json: return PrintJson(results.getXrayScanResults()) case Sarif: - sarifFile, err := GenerateSarifFileFromScan(results, isMultipleRoots, false) + sarifFile, err := GenerateSarifFileFromScan(results, isMultipleRoots, false, "JFrog Security", coreutils.JFrogComUrl+"xray/") if err != nil { return err } @@ -117,13 +124,13 @@ func printMessage(message string) { log.Output("💬", message) } -func GenerateSarifFileFromScan(extendedResults *ExtendedScanResults, isMultipleRoots, simplifiedOutput bool) (string, error) { +func GenerateSarifFileFromScan(extendedResults *ExtendedScanResults, isMultipleRoots, markdownOutput bool, scanningTool, toolURI string) (string, error) { report, err := sarif.New(sarif.Version210) if err != nil { return "", errorutils.CheckError(err) } - run := sarif.NewRunWithInformationURI("JFrog Xray", coreutils.JFrogComUrl+"xray/") - if err = convertScanToSarif(run, extendedResults, isMultipleRoots, simplifiedOutput); err != nil { + run := sarif.NewRunWithInformationURI(scanningTool, toolURI) + if err = convertScanToSarif(run, extendedResults, isMultipleRoots, markdownOutput); err != nil { return "", err } report.AddRun(run) @@ -174,16 +181,79 @@ func convertScanToSimpleJson(extendedResults *ExtendedScanResults, errors []form return jsonTable, nil } -func convertScanToSarif(run *sarif.Run, extendedResults *ExtendedScanResults, isMultipleRoots, simplifiedOutput bool) error { +func convertScanToSarif(run *sarif.Run, extendedResults *ExtendedScanResults, isMultipleRoots, markdownOutput bool) error { var errors []formats.SimpleJsonError - jsonTable, err := convertScanToSimpleJson(extendedResults, errors, isMultipleRoots, false, simplifiedOutput) + jsonTable, err := convertScanToSimpleJson(extendedResults, errors, isMultipleRoots, true, markdownOutput) if err != nil { return err } + if len(jsonTable.Vulnerabilities) > 0 || len(jsonTable.SecurityViolations) > 0 { + if err = convertToVulnerabilityOrViolationSarif(run, &jsonTable, markdownOutput); err != nil { + return err + } + } + return convertToIacOrSecretsSarif(run, &jsonTable, markdownOutput) +} + +func convertToVulnerabilityOrViolationSarif(run *sarif.Run, jsonTable *formats.SimpleJsonResults, markdownOutput bool) error { if len(jsonTable.SecurityViolations) > 0 { - return convertViolations(jsonTable, run, simplifiedOutput) + return convertViolationsToSarif(jsonTable, run, markdownOutput) + } + return convertVulnerabilitiesToSarif(jsonTable, run, markdownOutput) +} + +func convertToIacOrSecretsSarif(run *sarif.Run, jsonTable *formats.SimpleJsonResults, markdownOutput bool) error { + var err error + for _, secret := range jsonTable.Secrets { + properties := getIacOrSecretsProperties(secret, markdownOutput, true) + if err = addPropertiesToSarifRun(run, &properties); err != nil { + return err + } + } + + for _, iac := range jsonTable.Iacs { + properties := getIacOrSecretsProperties(iac, markdownOutput, false) + if err = addPropertiesToSarifRun(run, &properties); err != nil { + return err + } + } + return err +} + +func getIacOrSecretsProperties(secretOrIac formats.IacSecretsRow, markdownOutput, isSecret bool) sarifProperties { + file := strings.TrimPrefix(secretOrIac.File, string(os.PathSeparator)) + mapSeverityToScore := map[string]string{ + "": "0.0", + "low": "3.9", + "medium": "6.9", + "high": "8.9", + "critical": "10", + } + severity := mapSeverityToScore[strings.ToLower(secretOrIac.Severity)] + markdownDescription := "" + headline := "Infrastructure as Code Vulnerability" + secretOrFinding := "Finding" + typeOrScanner := "Scanner" + if isSecret { + secretOrFinding = "Secret" + typeOrScanner = "Type" + headline = "Potential Secret Exposed" + } + if markdownOutput { + headerRow := fmt.Sprintf("| Severity | File | Line:Column | %s | %s |\n", secretOrFinding, typeOrScanner) + separatorRow := "| :---: | :---: | :---: | :---: | :---: |\n" + tableHeader := headerRow + separatorRow + markdownDescription = tableHeader + fmt.Sprintf("| %s | %s | %s | %s | %s |", secretOrIac.Severity, file, secretOrIac.LineColumn, secretOrIac.Text, secretOrIac.Type) + } + return sarifProperties{ + Headline: headline, + Severity: severity, + Description: secretOrIac.Text, + MarkdownDescription: markdownDescription, + File: file, + LineColumn: secretOrIac.LineColumn, + SecretsOrIacType: secretOrIac.Type, } - return convertVulnerabilities(jsonTable, run, simplifiedOutput) } func getCves(cvesRow []formats.CveRow, issueId string) string { @@ -202,28 +272,25 @@ func getCves(cvesRow []formats.CveRow, issueId string) string { return cvesStr } -func getHeadline(impactedPackage, version, key, fixVersion string) string { - if fixVersion != "" { - return fmt.Sprintf("[%s] Upgrade %s:%s to %s", key, impactedPackage, version, fixVersion) - } - return fmt.Sprintf("[%s] %s:%s", key, impactedPackage, version) +func getVulnerabilityOrViolationSarifHeadline(depName, version, key string) string { + return fmt.Sprintf("[%s] %s %s", key, depName, version) } -func convertViolations(jsonTable formats.SimpleJsonResults, run *sarif.Run, simplifiedOutput bool) error { +func convertViolationsToSarif(jsonTable *formats.SimpleJsonResults, run *sarif.Run, markdownOutput bool) error { for _, violation := range jsonTable.SecurityViolations { - sarifProperties, err := getSarifProperties(violation, simplifiedOutput) + properties, err := getViolatedDepsSarifProps(violation, markdownOutput) if err != nil { return err } - err = addScanResultsToSarifRun(run, sarifProperties.Severity, violation.IssueId, sarifProperties.Headline, sarifProperties.Description, violation.Technology) - if err != nil { + if err = addPropertiesToSarifRun(run, &properties); err != nil { return err } } for _, license := range jsonTable.LicensesViolations { - impactedPackageFull := getHeadline(license.ImpactedDependencyName, license.ImpactedDependencyVersion, license.LicenseKey, "") - err := addScanResultsToSarifRun(run, "", license.ImpactedDependencyVersion, impactedPackageFull, license.LicenseKey, coreutils.Technology(strings.ToLower(license.ImpactedDependencyType))) - if err != nil { + if err := addPropertiesToSarifRun(run, + &sarifProperties{ + Severity: license.Severity, + Headline: getVulnerabilityOrViolationSarifHeadline(license.LicenseKey, license.ImpactedDependencyName, license.ImpactedDependencyVersion)}); err != nil { return err } } @@ -231,35 +298,39 @@ func convertViolations(jsonTable formats.SimpleJsonResults, run *sarif.Run, simp return nil } -func getSarifProperties(vulnerabilityRow formats.VulnerabilityOrViolationRow, simplifiedOutput bool) (sarifProperties, error) { +func getViolatedDepsSarifProps(vulnerabilityRow formats.VulnerabilityOrViolationRow, markdownOutput bool) (sarifProperties, error) { cves := getCves(vulnerabilityRow.Cves, vulnerabilityRow.IssueId) - fixVersion := getMinimalFixVersion(vulnerabilityRow.FixedVersions) - headline := getHeadline(vulnerabilityRow.ImpactedDependencyName, vulnerabilityRow.ImpactedDependencyVersion, cves, fixVersion) + headline := getVulnerabilityOrViolationSarifHeadline(vulnerabilityRow.ImpactedDependencyName, vulnerabilityRow.ImpactedDependencyVersion, cves) maxCveScore, err := findMaxCVEScore(vulnerabilityRow.Cves) if err != nil { return sarifProperties{}, err } - formattedDirectDependecies := getDirectDependenciesFormatted(vulnerabilityRow.Components) - description := vulnerabilityRow.Summary - if simplifiedOutput { - description = getDescription(formattedDirectDependecies, maxCveScore, vulnerabilityRow.FixedVersions) + formattedDirectDependencies, err := getDirectDependenciesFormatted(vulnerabilityRow.Components) + if err != nil { + return sarifProperties{}, err + } + markdownDescription := "" + if markdownOutput { + markdownDescription = getSarifTableDescription(formattedDirectDependencies, maxCveScore, vulnerabilityRow.Applicable, vulnerabilityRow.FixedVersions) + "\n" } return sarifProperties{ - Cves: cves, - Headline: headline, - Severity: maxCveScore, - Description: description, + Applicable: vulnerabilityRow.Applicable, + Cves: cves, + Headline: headline, + Severity: maxCveScore, + Description: vulnerabilityRow.Summary, + MarkdownDescription: markdownDescription, + File: vulnerabilityRow.Technology.GetPackageDescriptor(), }, err } -func convertVulnerabilities(jsonTable formats.SimpleJsonResults, run *sarif.Run, simplifiedOutput bool) error { +func convertVulnerabilitiesToSarif(jsonTable *formats.SimpleJsonResults, run *sarif.Run, simplifiedOutput bool) error { for _, vulnerability := range jsonTable.Vulnerabilities { - sarifProperties, err := getSarifProperties(vulnerability, simplifiedOutput) + properties, err := getViolatedDepsSarifProps(vulnerability, simplifiedOutput) if err != nil { return err } - err = addScanResultsToSarifRun(run, sarifProperties.Severity, vulnerability.IssueId, sarifProperties.Headline, sarifProperties.Description, vulnerability.Technology) - if err != nil { + if err = addPropertiesToSarifRun(run, &properties); err != nil { return err } } @@ -267,54 +338,83 @@ func convertVulnerabilities(jsonTable formats.SimpleJsonResults, run *sarif.Run, return nil } -func getDirectDependenciesFormatted(directDependencies []formats.ComponentRow) string { +func getDirectDependenciesFormatted(directDependencies []formats.ComponentRow) (string, error) { var formattedDirectDependencies strings.Builder for _, dependency := range directDependencies { - formattedDirectDependencies.WriteString(fmt.Sprintf("`%s:%s`, ", dependency.Name, dependency.Version)) + if _, err := formattedDirectDependencies.WriteString(fmt.Sprintf("`%s %s`
", dependency.Name, dependency.Version)); err != nil { + return "", err + } } - return strings.TrimSuffix(formattedDirectDependencies.String(), ", ") + return strings.TrimSuffix(formattedDirectDependencies.String(), "
"), nil } -func getDescription(formattedDirectDependencies, maxCveScore string, fixedVersions []string) string { +func getSarifTableDescription(formattedDirectDependencies, maxCveScore, applicable string, fixedVersions []string) string { descriptionFixVersions := "No fix available" if len(fixedVersions) > 0 { descriptionFixVersions = strings.Join(fixedVersions, ", ") } - return fmt.Sprintf("| Severity Score | Direct Dependencies | Fixed Versions |\n| :--- | :----: | ---: |\n| %s | %s | %s |", - maxCveScore, formattedDirectDependencies, descriptionFixVersions) -} - -func getMinimalFixVersion(fixVersions []string) string { - if len(fixVersions) > 0 { - return fixVersions[0] + if applicable == "" { + return fmt.Sprintf("| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| %s | %s | %s |", + maxCveScore, formattedDirectDependencies, descriptionFixVersions) } - return "" + return fmt.Sprintf("| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| %s | %s | %s | %s |", + maxCveScore, applicable, formattedDirectDependencies, descriptionFixVersions) } // Adding the Xray scan results details to the sarif struct, for each issue found in the scan -func addScanResultsToSarifRun(run *sarif.Run, severity, issueId, impactedPackage, description string, technology coreutils.Technology) error { - techPackageDescriptor := technology.GetPackageDescriptor() +func addPropertiesToSarifRun(run *sarif.Run, properties *sarifProperties) error { pb := sarif.NewPropertyBag() - if severity != missingCveScore { - pb.Add("security-severity", severity) + if properties.Severity != missingCveScore { + pb.Add("security-severity", properties.Severity) + } + description := properties.Description + markdownDescription := properties.MarkdownDescription + if markdownDescription != "" { + description = "" + } + line := 0 + column := 0 + var err error + if properties.LineColumn != "" { + lineColumn := strings.Split(properties.LineColumn, ":") + if line, err = strconv.Atoi(lineColumn[0]); err != nil { + return err + } + if column, err = strconv.Atoi(lineColumn[1]); err != nil { + return err + } } - run.AddRule(issueId). + ruleID := generateSarifRuleID(properties) + run.AddRule(ruleID). + WithDescription(description). WithProperties(pb.Properties). - WithMarkdownHelp(description) - run.CreateResultForRule(issueId). - WithMessage(sarif.NewTextMessage(impactedPackage)). + WithMarkdownHelp(markdownDescription) + run.CreateResultForRule(ruleID). + WithMessage(sarif.NewTextMessage(properties.Headline)). AddLocation( sarif.NewLocationWithPhysicalLocation( sarif.NewPhysicalLocation(). WithArtifactLocation( - sarif.NewSimpleArtifactLocation(techPackageDescriptor), - ), + sarif.NewSimpleArtifactLocation(properties.File), + ).WithRegion( + sarif.NewSimpleRegion(line, line). + WithStartColumn(column)), ), ) - return nil } +func generateSarifRuleID(properties *sarifProperties) string { + switch { + case properties.Cves != "": + return properties.Cves + case properties.XrayID != "": + return properties.XrayID + default: + return properties.File + } +} + func findMaxCVEScore(cves []formats.CveRow) (string, error) { maxCve := 0.0 for _, cve := range cves { diff --git a/xray/utils/resultwriter_test.go b/xray/utils/resultwriter_test.go index 4af999987..252112826 100644 --- a/xray/utils/resultwriter_test.go +++ b/xray/utils/resultwriter_test.go @@ -1,51 +1,99 @@ package utils import ( + "fmt" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/formats" "github.com/jfrog/jfrog-client-go/xray/services" "github.com/stretchr/testify/assert" + "path" "testing" ) func TestGenerateSarifFileFromScan(t *testing.T) { - currentScan := services.ScanResponse{ - Vulnerabilities: []services.Vulnerability{ + extendedResults := &ExtendedScanResults{ + XrayResults: []services.ScanResponse{ { - IssueId: "XRAY-1", - Summary: "summary-1", - Cves: []services.Cve{ + Vulnerabilities: []services.Vulnerability{ { - Id: "CVE-2022-0000", - CvssV3Score: "9", + Cves: []services.Cve{{Id: "CVE-2022-1234", CvssV3Score: "8.0"}, {Id: "CVE-2023-1234", CvssV3Score: "7.1"}}, + Summary: "A test vulnerability the harms nothing", + Severity: "High", + Components: map[string]services.Component{ + "vulnerability1": {FixedVersions: []string{"1.2.3"}}, + }, + Technology: coreutils.Go.ToString(), }, - }, - Components: map[string]services.Component{ - "component-G": { - FixedVersions: []string{"[2.1.3]"}, - ImpactPaths: nil, + { + Summary: "A test vulnerability the harms nothing", + Severity: "High", + Components: map[string]services.Component{ + "vulnerability2": {}, + }, + IssueId: "XRAY-1234", + Technology: coreutils.Go.ToString(), }, }, - Technology: "go", }, }, - ScannedPackageType: "Go", + SecretsScanResults: []IacOrSecretResult{ + { + Severity: "Medium", + File: "found_secrets.js", + LineColumn: "1:18", + Type: "entropy", + Text: "AAA************", + }, + }, + IacScanResults: []IacOrSecretResult{ + { + Severity: "Medium", + File: "plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json", + LineColumn: "229:38", + Type: "entropy", + Text: "BBB************", + }, + }, + } + testCases := []struct { + name string + extendedResults *ExtendedScanResults + isMultipleRoots bool + markdownOutput bool + expectedSarifOutput string + }{ + { + name: "Scan results with vulnerabilities, secrets and IaC", + extendedResults: extendedResults, + expectedSarifOutput: "{\n \"version\": \"2.1.0\",\n \"$schema\": \"https://json.schemastore.org/sarif-2.1.0-rtm.5.json\",\n \"runs\": [\n {\n \"tool\": {\n \"driver\": {\n \"informationUri\": \"https://example.com/\",\n \"name\": \"JFrog Security\",\n \"rules\": [\n {\n \"id\": \"CVE-2022-1234, CVE-2023-1234\",\n \"shortDescription\": {\n \"text\": \"A test vulnerability the harms nothing\"\n },\n \"help\": {\n \"markdown\": \"\"\n },\n \"properties\": {\n \"security-severity\": \"8.0\"\n }\n },\n {\n \"id\": \"XRAY-1234\",\n \"shortDescription\": {\n \"text\": \"A test vulnerability the harms nothing\"\n },\n \"help\": {\n \"markdown\": \"\"\n },\n \"properties\": {\n \"security-severity\": \"0.0\"\n }\n },\n {\n \"id\": \"found_secrets.js\",\n \"shortDescription\": {\n \"text\": \"AAA************\"\n },\n \"help\": {\n \"markdown\": \"\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n },\n {\n \"id\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"shortDescription\": {\n \"text\": \"BBB************\"\n },\n \"help\": {\n \"markdown\": \"\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n }\n ]\n }\n },\n \"results\": [\n {\n \"ruleId\": \"CVE-2022-1234, CVE-2023-1234\",\n \"ruleIndex\": 0,\n \"message\": {\n \"text\": \"[CVE-2022-1234, CVE-2023-1234] vulnerability1 \"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"go.mod\"\n },\n \"region\": {\n \"startLine\": 0,\n \"startColumn\": 0,\n \"endLine\": 0\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"XRAY-1234\",\n \"ruleIndex\": 1,\n \"message\": {\n \"text\": \"[XRAY-1234] vulnerability2 \"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"go.mod\"\n },\n \"region\": {\n \"startLine\": 0,\n \"startColumn\": 0,\n \"endLine\": 0\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"found_secrets.js\",\n \"ruleIndex\": 2,\n \"message\": {\n \"text\": \"Potential Secret Exposed\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"found_secrets.js\"\n },\n \"region\": {\n \"startLine\": 1,\n \"startColumn\": 18,\n \"endLine\": 1\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"ruleIndex\": 3,\n \"message\": {\n \"text\": \"Infrastructure as Code Vulnerability\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\"\n },\n \"region\": {\n \"startLine\": 229,\n \"startColumn\": 38,\n \"endLine\": 229\n }\n }\n }\n ]\n }\n ]\n }\n ]\n}", + }, + { + name: "Scan results with vulnerabilities, secrets and IaC as Markdown", + extendedResults: extendedResults, + markdownOutput: true, + expectedSarifOutput: "{\n \"version\": \"2.1.0\",\n \"$schema\": \"https://json.schemastore.org/sarif-2.1.0-rtm.5.json\",\n \"runs\": [\n {\n \"tool\": {\n \"driver\": {\n \"informationUri\": \"https://example.com/\",\n \"name\": \"JFrog Security\",\n \"rules\": [\n {\n \"id\": \"CVE-2022-1234, CVE-2023-1234\",\n \"shortDescription\": {\n \"text\": \"\"\n },\n \"help\": {\n \"markdown\": \"| Severity Score | Direct Dependencies | Fixed Versions |\\n| :---: | :----: | :---: |\\n| 8.0 | | 1.2.3 |\\n\"\n },\n \"properties\": {\n \"security-severity\": \"8.0\"\n }\n },\n {\n \"id\": \"XRAY-1234\",\n \"shortDescription\": {\n \"text\": \"\"\n },\n \"help\": {\n \"markdown\": \"| Severity Score | Direct Dependencies | Fixed Versions |\\n| :---: | :----: | :---: |\\n| 0.0 | | No fix available |\\n\"\n },\n \"properties\": {\n \"security-severity\": \"0.0\"\n }\n },\n {\n \"id\": \"found_secrets.js\",\n \"shortDescription\": {\n \"text\": \"\"\n },\n \"help\": {\n \"markdown\": \"| Severity | File | Line:Column | Secret | Type |\\n| :---: | :---: | :---: | :---: | :---: |\\n| Medium | found_secrets.js | 1:18 | AAA************ | entropy |\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n },\n {\n \"id\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"shortDescription\": {\n \"text\": \"\"\n },\n \"help\": {\n \"markdown\": \"| Severity | File | Line:Column | Finding | Scanner |\\n| :---: | :---: | :---: | :---: | :---: |\\n| Medium | plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json | 229:38 | BBB************ | entropy |\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n }\n ]\n }\n },\n \"results\": [\n {\n \"ruleId\": \"CVE-2022-1234, CVE-2023-1234\",\n \"ruleIndex\": 0,\n \"message\": {\n \"text\": \"[CVE-2022-1234, CVE-2023-1234] vulnerability1 \"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"go.mod\"\n },\n \"region\": {\n \"startLine\": 0,\n \"startColumn\": 0,\n \"endLine\": 0\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"XRAY-1234\",\n \"ruleIndex\": 1,\n \"message\": {\n \"text\": \"[XRAY-1234] vulnerability2 \"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"go.mod\"\n },\n \"region\": {\n \"startLine\": 0,\n \"startColumn\": 0,\n \"endLine\": 0\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"found_secrets.js\",\n \"ruleIndex\": 2,\n \"message\": {\n \"text\": \"Potential Secret Exposed\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"found_secrets.js\"\n },\n \"region\": {\n \"startLine\": 1,\n \"startColumn\": 18,\n \"endLine\": 1\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"ruleIndex\": 3,\n \"message\": {\n \"text\": \"Infrastructure as Code Vulnerability\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\"\n },\n \"region\": {\n \"startLine\": 229,\n \"startColumn\": 38,\n \"endLine\": 229\n }\n }\n }\n ]\n }\n ]\n }\n ]\n}", + }, + { + name: "Scan results without vulnerabilities", + extendedResults: &ExtendedScanResults{}, + isMultipleRoots: true, + markdownOutput: true, + expectedSarifOutput: "{\n \"version\": \"2.1.0\",\n \"$schema\": \"https://json.schemastore.org/sarif-2.1.0-rtm.5.json\",\n \"runs\": [\n {\n \"tool\": {\n \"driver\": {\n \"informationUri\": \"https://example.com/\",\n \"name\": \"JFrog Security\",\n \"rules\": []\n }\n },\n \"results\": []\n }\n ]\n}", + }, } - var scanResults = []services.ScanResponse{currentScan} - extendedResults := &ExtendedScanResults{XrayResults: scanResults} - sarif, err := GenerateSarifFileFromScan(extendedResults, false, false) - assert.NoError(t, err) - expected := "{\"version\":\"2.1.0\",\"$schema\":\"https://json.schemastore.org/sarif-2.1.0-rtm.5.json\",\"runs\":[{\"tool\":{\"driver\":{\"informationUri\":\"https://jfrog.com/xray/\",\"name\":\"JFrog Xray\",\"rules\":[{\"id\":\"XRAY-1\",\"shortDescription\":null,\"help\":{\"markdown\":\"summary-1\"},\"properties\":{\"security-severity\":\"9.0\"}}]}},\"results\":[{\"ruleId\":\"XRAY-1\",\"ruleIndex\":0,\"message\":{\"text\":\"[CVE-2022-0000] Upgrade component-G: to [2.1.3]\"},\"locations\":[{\"physicalLocation\":{\"artifactLocation\":{\"uri\":\"go.mod\"}}}]}]}]}" - assert.JSONEq(t, expected, sarif) - sarif, err = GenerateSarifFileFromScan(extendedResults, false, true) - assert.NoError(t, err) - expected = "{\n \"version\": \"2.1.0\",\n \"$schema\": \"https://json.schemastore.org/sarif-2.1.0-rtm.5.json\",\n \"runs\": [\n {\n \"tool\": {\n \"driver\": {\n \"informationUri\": \"https://jfrog.com/xray/\",\n \"name\": \"JFrog Xray\",\n \"rules\": [\n {\n \"id\": \"XRAY-1\",\n \"shortDescription\": null,\n \"help\": {\n \"markdown\": \"| Severity Score | Direct Dependencies | Fixed Versions |\\n| :--- | :----: | ---: |\\n| 9.0 | | [2.1.3] |\"\n },\n \"properties\": {\n \"security-severity\": \"9.0\"\n }\n }\n ]\n }\n },\n \"results\": [\n {\n \"ruleId\": \"XRAY-1\",\n \"ruleIndex\": 0,\n \"message\": {\n \"text\": \"[CVE-2022-0000] Upgrade component-G: to [2.1.3]\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"go.mod\"\n }\n }\n }\n ]\n }\n ]\n }\n ]\n}" - assert.JSONEq(t, expected, sarif) + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + sarifOutput, err := GenerateSarifFileFromScan(testCase.extendedResults, testCase.isMultipleRoots, testCase.markdownOutput, "JFrog Security", "https://example.com/") + assert.NoError(t, err) + assert.Equal(t, testCase.expectedSarifOutput, sarifOutput) + }) + } } -func TestGetHeadline(t *testing.T) { - assert.Equal(t, "[CVE-2022-1234] Upgrade loadsh:1.4.1 to 2.0.0", getHeadline("loadsh", "1.4.1", "CVE-2022-1234", "2.0.0")) - assert.NotEqual(t, "[CVE-2022-1234] Upgrade loadsh:1.4.1 to 2.0.0", getHeadline("loadsh", "1.2.1", "CVE-2022-1234", "2.0.0")) +func TestGetVulnerabilityOrViolationSarifHeadline(t *testing.T) { + assert.Equal(t, "[CVE-2022-1234] loadsh 1.4.1", getVulnerabilityOrViolationSarifHeadline("loadsh", "1.4.1", "CVE-2022-1234")) + assert.NotEqual(t, "[CVE-2022-1234] loadsh 1.4.1", getVulnerabilityOrViolationSarifHeadline("loadsh", "1.2.1", "CVE-2022-1234")) } func TestGetCves(t *testing.T) { @@ -56,3 +104,273 @@ func TestGetCves(t *testing.T) { assert.Equal(t, "CVE-2022-1234, CVE-2019-1234", getCves(cvesRow, issueId)) assert.Equal(t, issueId, getCves(nil, issueId)) } + +func TestGetIacOrSecretsProperties(t *testing.T) { + testCases := []struct { + name string + secretOrIac formats.IacSecretsRow + markdownOutput bool + isSecret bool + expectedOutput sarifProperties + }{ + { + name: "Infrastructure as Code vulnerability without markdown output", + secretOrIac: formats.IacSecretsRow{ + Severity: "high", + File: path.Join("path", "to", "file"), + LineColumn: "10:5", + Text: "Vulnerable code", + Type: "Terraform", + }, + markdownOutput: false, + isSecret: false, + expectedOutput: sarifProperties{ + Applicable: "", + Cves: "", + Headline: "Infrastructure as Code Vulnerability", + Severity: "8.9", + Description: "Vulnerable code", + MarkdownDescription: "", + XrayID: "", + File: path.Join("path", "to", "file"), + LineColumn: "10:5", + SecretsOrIacType: "Terraform", + }, + }, + { + name: "Potential secret exposed with markdown output", + secretOrIac: formats.IacSecretsRow{ + Severity: "medium", + File: path.Join("path", "to", "file"), + LineColumn: "5:3", + Text: "Potential secret", + Type: "AWS Secret Manager", + }, + markdownOutput: true, + isSecret: true, + expectedOutput: sarifProperties{ + Applicable: "", + Cves: "", + Headline: "Potential Secret Exposed", + Severity: "6.9", + Description: "Potential secret", + MarkdownDescription: fmt.Sprintf("| Severity | File | Line:Column | Secret | Type |\n| :---: | :---: | :---: | :---: | :---: |\n| medium | %s | 5:3 | Potential secret | AWS Secret Manager |", path.Join("path", "to", "file")), + XrayID: "", + File: path.Join("path", "to", "file"), + LineColumn: "5:3", + SecretsOrIacType: "AWS Secret Manager", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + output := getIacOrSecretsProperties(testCase.secretOrIac, testCase.markdownOutput, testCase.isSecret) + assert.Equal(t, testCase.expectedOutput.Applicable, output.Applicable) + assert.Equal(t, testCase.expectedOutput.Cves, output.Cves) + assert.Equal(t, testCase.expectedOutput.Headline, output.Headline) + assert.Equal(t, testCase.expectedOutput.Severity, output.Severity) + assert.Equal(t, testCase.expectedOutput.Description, output.Description) + assert.Equal(t, testCase.expectedOutput.MarkdownDescription, output.MarkdownDescription) + assert.Equal(t, testCase.expectedOutput.XrayID, output.XrayID) + assert.Equal(t, testCase.expectedOutput.File, output.File) + assert.Equal(t, testCase.expectedOutput.LineColumn, output.LineColumn) + assert.Equal(t, testCase.expectedOutput.SecretsOrIacType, output.SecretsOrIacType) + }) + } +} + +func TestGetViolatedDepsSarifProps(t *testing.T) { + testCases := []struct { + name string + vulnerability formats.VulnerabilityOrViolationRow + markdownOutput bool + expectedOutput sarifProperties + }{ + { + name: "Vulnerability with markdown output", + vulnerability: formats.VulnerabilityOrViolationRow{ + Summary: "Vulnerable dependency", + Severity: "high", + Applicable: "Applicable", + ImpactedDependencyName: "example-package", + ImpactedDependencyVersion: "1.0.0", + ImpactedDependencyType: "npm", + FixedVersions: []string{"1.0.1", "1.0.2"}, + Components: []formats.ComponentRow{ + {Name: "example-package", Version: "1.0.0"}, + }, + Cves: []formats.CveRow{ + {Id: "CVE-2021-1234", CvssV3: "7.2"}, + {Id: "CVE-2021-5678", CvssV3: "7.2"}, + }, + IssueId: "XRAY-12345", + }, + markdownOutput: true, + expectedOutput: sarifProperties{ + Applicable: "Applicable", + Cves: "CVE-2021-1234, CVE-2021-5678", + Headline: "[CVE-2021-1234, CVE-2021-5678] example-package 1.0.0", + Severity: "7.2", + Description: "Vulnerable dependency", + MarkdownDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 7.2 | Applicable | `example-package 1.0.0` | 1.0.1, 1.0.2 |\n", + }, + }, + { + name: "Vulnerability without markdown output", + vulnerability: formats.VulnerabilityOrViolationRow{ + Summary: "Vulnerable dependency", + Severity: "high", + Applicable: "Applicable", + ImpactedDependencyName: "example-package", + ImpactedDependencyVersion: "1.0.0", + ImpactedDependencyType: "npm", + FixedVersions: []string{"1.0.1", "1.0.2"}, + Components: []formats.ComponentRow{ + {Name: "example-package", Version: "1.0.0"}, + }, + Cves: []formats.CveRow{ + {Id: "CVE-2021-1234", CvssV3: "7.2"}, + {Id: "CVE-2021-5678", CvssV3: "7.2"}, + }, + IssueId: "XRAY-12345", + }, + expectedOutput: sarifProperties{ + Applicable: "Applicable", + Cves: "CVE-2021-1234, CVE-2021-5678", + Headline: "[CVE-2021-1234, CVE-2021-5678] example-package 1.0.0", + Severity: "7.2", + Description: "Vulnerable dependency", + MarkdownDescription: "", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output, err := getViolatedDepsSarifProps(tc.vulnerability, tc.markdownOutput) + assert.NoError(t, err) + assert.Equal(t, tc.expectedOutput.Cves, output.Cves) + assert.Equal(t, tc.expectedOutput.Severity, output.Severity) + assert.Equal(t, tc.expectedOutput.XrayID, output.XrayID) + assert.Equal(t, tc.expectedOutput.MarkdownDescription, output.MarkdownDescription) + assert.Equal(t, tc.expectedOutput.Applicable, output.Applicable) + assert.Equal(t, tc.expectedOutput.Description, output.Description) + assert.Equal(t, tc.expectedOutput.Headline, output.Headline) + }) + } +} + +func TestGetDirectDependenciesFormatted(t *testing.T) { + testCases := []struct { + name string + directDeps []formats.ComponentRow + expectedOutput string + }{ + { + name: "Single direct dependency", + directDeps: []formats.ComponentRow{ + {Name: "example-package", Version: "1.0.0"}, + }, + expectedOutput: "`example-package 1.0.0`", + }, + { + name: "Multiple direct dependencies", + directDeps: []formats.ComponentRow{ + {Name: "dependency1", Version: "1.0.0"}, + {Name: "dependency2", Version: "2.0.0"}, + }, + expectedOutput: "`dependency1 1.0.0`
`dependency2 2.0.0`", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output, err := getDirectDependenciesFormatted(tc.directDeps) + assert.NoError(t, err) + assert.Equal(t, tc.expectedOutput, output) + }) + } +} + +func TestGetSarifTableDescription(t *testing.T) { + testCases := []struct { + name string + formattedDeps string + maxCveScore string + applicable string + fixedVersions []string + expectedDescription string + }{ + { + name: "Applicable vulnerability", + formattedDeps: "`example-package 1.0.0`", + maxCveScore: "7.5", + applicable: "Applicable", + fixedVersions: []string{"1.0.1", "1.0.2"}, + expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 7.5 | Applicable | `example-package 1.0.0` | 1.0.1, 1.0.2 |", + }, + { + name: "Non-applicable vulnerability", + formattedDeps: "`example-package 2.0.0`", + maxCveScore: "6.2", + applicable: "", + fixedVersions: []string{"2.0.1"}, + expectedDescription: "| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| 6.2 | `example-package 2.0.0` | 2.0.1 |", + }, + { + name: "No fixed versions", + formattedDeps: "`example-package 3.0.0`", + maxCveScore: "3.0", + applicable: "", + fixedVersions: []string{}, + expectedDescription: "| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| 3.0 | `example-package 3.0.0` | No fix available |", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output := getSarifTableDescription(tc.formattedDeps, tc.maxCveScore, tc.applicable, tc.fixedVersions) + assert.Equal(t, tc.expectedDescription, output) + }) + } +} + +func TestFindMaxCVEScore(t *testing.T) { + testCases := []struct { + name string + cves []formats.CveRow + expectedOutput string + expectedError bool + }{ + { + name: "CVEScore with valid float values", + cves: []formats.CveRow{ + {Id: "CVE-2021-1234", CvssV3: "7.5"}, + {Id: "CVE-2021-5678", CvssV3: "9.2"}, + }, + expectedOutput: "9.2", + }, + { + name: "CVEScore with invalid float value", + cves: []formats.CveRow{ + {Id: "CVE-2022-4321", CvssV3: "invalid"}, + }, + expectedOutput: "", + expectedError: true, + }, + { + name: "CVEScore without values", + cves: []formats.CveRow{}, + expectedOutput: "0.0", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output, err := findMaxCVEScore(tc.cves) + assert.False(t, tc.expectedError && err == nil) + assert.Equal(t, tc.expectedOutput, output) + }) + } +}