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)
+ })
+ }
+}