diff --git a/utils/coreutils/techutils.go b/utils/coreutils/techutils.go index 58ea37a44..3880da6ad 100644 --- a/utils/coreutils/techutils.go +++ b/utils/coreutils/techutils.go @@ -44,8 +44,8 @@ type TechData struct { ciSetupSupport bool // Whether Contextual Analysis supported in this technology. applicabilityScannable bool - // The file that handles the project's dependencies. - packageDescriptor string + // The files that handle the project's dependencies. + packageDescriptors []string // Formal name of the technology formal string // The executable name of the technology @@ -60,21 +60,21 @@ var technologiesData = map[Technology]TechData{ Maven: { indicators: []string{"pom.xml"}, ciSetupSupport: true, - packageDescriptor: "pom.xml", + packageDescriptors: []string{"pom.xml"}, execCommand: "mvn", applicabilityScannable: true, }, Gradle: { indicators: []string{".gradle", ".gradle.kts"}, ciSetupSupport: true, - packageDescriptor: "build.gradle, build.gradle.kts", + packageDescriptors: []string{"build.gradle", "build.gradle.kts"}, applicabilityScannable: true, }, Npm: { indicators: []string{"package.json", "package-lock.json", "npm-shrinkwrap.json"}, exclude: []string{".yarnrc.yml", "yarn.lock", ".yarn"}, ciSetupSupport: true, - packageDescriptor: "package.json", + packageDescriptors: []string{"package.json"}, formal: string(Npm), packageVersionOperator: "@", packageInstallationCommand: "install", @@ -82,26 +82,27 @@ var technologiesData = map[Technology]TechData{ }, Yarn: { indicators: []string{".yarnrc.yml", "yarn.lock", ".yarn"}, - packageDescriptor: "package.json", + packageDescriptors: []string{"package.json"}, packageVersionOperator: "@", applicabilityScannable: true, }, Go: { indicators: []string{"go.mod"}, - packageDescriptor: "go.mod", + packageDescriptors: []string{"go.mod"}, packageVersionOperator: "@v", packageInstallationCommand: "get", }, Pip: { packageType: Pypi, indicators: []string{"setup.py", "requirements.txt"}, + packageDescriptors: []string{"setup.py", "requirements.txt"}, exclude: []string{"Pipfile", "Pipfile.lock", "pyproject.toml", "poetry.lock"}, applicabilityScannable: true, }, Pipenv: { packageType: Pypi, indicators: []string{"Pipfile", "Pipfile.lock"}, - packageDescriptor: "Pipfile", + packageDescriptors: []string{"Pipfile"}, packageVersionOperator: "==", packageInstallationCommand: "install", applicabilityScannable: true, @@ -153,11 +154,8 @@ func (tech Technology) GetPackageType() string { return technologiesData[tech].packageType } -func (tech Technology) GetPackageDescriptor() string { - if technologiesData[tech].packageDescriptor == "" { - return tech.ToFormal() + " Package Descriptor" - } - return technologiesData[tech].packageDescriptor +func (tech Technology) GetPackageDescriptor() []string { + return technologiesData[tech].packageDescriptors } func (tech Technology) IsCiSetup() bool { diff --git a/xray/commands/audit/jas/applicability/applicabilitymanager.go b/xray/commands/audit/jas/applicability/applicabilitymanager.go index c34318c00..202e3d9ff 100644 --- a/xray/commands/audit/jas/applicability/applicabilitymanager.go +++ b/xray/commands/audit/jas/applicability/applicabilitymanager.go @@ -17,8 +17,9 @@ import ( ) const ( - applicabilityScanType = "analyze-applicability" - applicabilityScanCommand = "ca" + applicabilityScanType = "analyze-applicability" + applicabilityScanCommand = "ca" + applicabilityDocsUrlSuffix = "contextual-analysis" ) type ApplicabilityScanManager struct { @@ -116,7 +117,7 @@ func (asm *ApplicabilityScanManager) Run(module jfrogappsconfig.Module) (err err if err = asm.runAnalyzerManager(); err != nil { return } - workingDirResults, err := jas.ReadJasScanRunsFromFile(asm.scanner.ResultsFileName, module.SourceRoot) + workingDirResults, err := jas.ReadJasScanRunsFromFile(asm.scanner.ResultsFileName, module.SourceRoot, applicabilityDocsUrlSuffix) if err != nil { return } diff --git a/xray/commands/audit/jas/applicability/applicabilitymanager_test.go b/xray/commands/audit/jas/applicability/applicabilitymanager_test.go index 98d54d28b..554909181 100644 --- a/xray/commands/audit/jas/applicability/applicabilitymanager_test.go +++ b/xray/commands/audit/jas/applicability/applicabilitymanager_test.go @@ -282,7 +282,7 @@ func TestParseResults_EmptyResults_AllCvesShouldGetUnknown(t *testing.T) { // Act var err error - applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot) + applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, applicabilityDocsUrlSuffix) if assert.NoError(t, err) { assert.Len(t, applicabilityManager.applicabilityScanResults, 1) @@ -299,7 +299,7 @@ func TestParseResults_ApplicableCveExist(t *testing.T) { // Act var err error - applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot) + applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, applicabilityDocsUrlSuffix) if assert.NoError(t, err) && assert.NotNil(t, applicabilityManager.applicabilityScanResults) { assert.Len(t, applicabilityManager.applicabilityScanResults, 1) @@ -316,7 +316,7 @@ func TestParseResults_AllCvesNotApplicable(t *testing.T) { // Act var err error - applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot) + applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, applicabilityDocsUrlSuffix) if assert.NoError(t, err) && assert.NotNil(t, applicabilityManager.applicabilityScanResults) { assert.Len(t, applicabilityManager.applicabilityScanResults, 1) diff --git a/xray/commands/audit/jas/common.go b/xray/commands/audit/jas/common.go index fa7d508ef..be165aa32 100644 --- a/xray/commands/audit/jas/common.go +++ b/xray/commands/audit/jas/common.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" "testing" + "unicode" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" @@ -127,7 +128,7 @@ func deleteJasProcessFiles(configFile string, resultFile string) error { return errorutils.CheckError(err) } -func ReadJasScanRunsFromFile(fileName, wd string) (sarifRuns []*sarif.Run, err error) { +func ReadJasScanRunsFromFile(fileName, wd, informationUrlSuffix string) (sarifRuns []*sarif.Run, err error) { if sarifRuns, err = utils.ReadScanRunsFromFile(fileName); err != nil { return } @@ -137,12 +138,31 @@ func ReadJasScanRunsFromFile(fileName, wd string) (sarifRuns []*sarif.Run, err e // Also used to calculate relative paths if needed with it sarifRun.Invocations[0].WorkingDirectory.WithUri(wd) // Process runs values + fillMissingRequiredDriverInformation(utils.BaseDocumentationURL+informationUrlSuffix, utils.GetAnalyzerManagerVersion(), sarifRun) sarifRun.Results = excludeSuppressResults(sarifRun.Results) addScoreToRunRules(sarifRun) } return } +func fillMissingRequiredDriverInformation(defaultJasInformationUri, defaultVersion string, run *sarif.Run) { + driver := run.Tool.Driver + if driver.InformationURI == nil { + driver.InformationURI = &defaultJasInformationUri + } + if driver.Version == nil || !isValidVersion(*driver.Version) { + driver.Version = &defaultVersion + } +} + +func isValidVersion(version string) bool { + if len(version) == 0 { + return false + } + firstChar := rune(version[0]) + return unicode.IsDigit(firstChar) +} + func excludeSuppressResults(sarifResults []*sarif.Result) []*sarif.Result { results := []*sarif.Result{} for _, sarifResult := range sarifResults { diff --git a/xray/commands/audit/jas/commons_test.go b/xray/commands/audit/jas/commons_test.go index d600d7a73..c43cf94ab 100644 --- a/xray/commands/audit/jas/commons_test.go +++ b/xray/commands/audit/jas/commons_test.go @@ -126,4 +126,4 @@ func TestGetExcludePatterns(t *testing.T) { assert.ElementsMatch(t, actualExcludePatterns, expectedExcludePatterns) }) } -} \ No newline at end of file +} diff --git a/xray/commands/audit/jas/iac/iacscanner.go b/xray/commands/audit/jas/iac/iacscanner.go index f2ea4984b..402557e3e 100644 --- a/xray/commands/audit/jas/iac/iacscanner.go +++ b/xray/commands/audit/jas/iac/iacscanner.go @@ -12,8 +12,9 @@ import ( ) const ( - iacScannerType = "iac-scan-modules" - iacScanCommand = "iac" + iacScannerType = "iac-scan-modules" + iacScanCommand = "iac" + iacDocsUrlSuffix = "infrastructure-as-code-iac" ) type IacScanManager struct { @@ -60,7 +61,7 @@ func (iac *IacScanManager) Run(module jfrogappsconfig.Module) (err error) { if err = iac.runAnalyzerManager(); err != nil { return } - workingDirResults, err := jas.ReadJasScanRunsFromFile(iac.scanner.ResultsFileName, module.SourceRoot) + workingDirResults, err := jas.ReadJasScanRunsFromFile(iac.scanner.ResultsFileName, module.SourceRoot, iacDocsUrlSuffix) if err != nil { return } diff --git a/xray/commands/audit/jas/iac/iacscanner_test.go b/xray/commands/audit/jas/iac/iacscanner_test.go index 595715bfd..1f9482ded 100644 --- a/xray/commands/audit/jas/iac/iacscanner_test.go +++ b/xray/commands/audit/jas/iac/iacscanner_test.go @@ -59,7 +59,7 @@ func TestIacParseResults_EmptyResults(t *testing.T) { // Act var err error - iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot) + iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, iacDocsUrlSuffix) if assert.NoError(t, err) && assert.NotNil(t, iacScanManager.iacScannerResults) { assert.Len(t, iacScanManager.iacScannerResults, 1) assert.Empty(t, iacScanManager.iacScannerResults[0].Results) @@ -75,7 +75,7 @@ func TestIacParseResults_ResultsContainIacViolations(t *testing.T) { // Act var err error - iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot) + iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, iacDocsUrlSuffix) if assert.NoError(t, err) && assert.NotNil(t, iacScanManager.iacScannerResults) { assert.Len(t, iacScanManager.iacScannerResults, 1) assert.Len(t, iacScanManager.iacScannerResults[0].Results, 4) diff --git a/xray/commands/audit/jas/sast/sastscanner.go b/xray/commands/audit/jas/sast/sastscanner.go index 2052a5361..578bc6775 100644 --- a/xray/commands/audit/jas/sast/sastscanner.go +++ b/xray/commands/audit/jas/sast/sastscanner.go @@ -14,8 +14,9 @@ import ( ) const ( - sastScannerType = "sast" - sastScanCommand = "zd" + sastScannerType = "sast" + sastScanCommand = "zd" + sastDocsUrlSuffix = "sast" ) type SastScanManager struct { @@ -55,7 +56,7 @@ func (ssm *SastScanManager) Run(module jfrogappsconfig.Module) (err error) { if err = ssm.runAnalyzerManager(filepath.Dir(ssm.scanner.AnalyzerManager.AnalyzerManagerFullPath)); err != nil { return } - workingDirRuns, err := jas.ReadJasScanRunsFromFile(scanner.ResultsFileName, module.SourceRoot) + workingDirRuns, err := jas.ReadJasScanRunsFromFile(scanner.ResultsFileName, module.SourceRoot, sastDocsUrlSuffix) if err != nil { return } diff --git a/xray/commands/audit/jas/sast/sastscanner_test.go b/xray/commands/audit/jas/sast/sastscanner_test.go index 036c028c8..43eedf705 100644 --- a/xray/commands/audit/jas/sast/sastscanner_test.go +++ b/xray/commands/audit/jas/sast/sastscanner_test.go @@ -36,7 +36,7 @@ func TestSastParseResults_EmptyResults(t *testing.T) { // Act var err error - sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot) + sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, sastDocsUrlSuffix) // Assert if assert.NoError(t, err) && assert.NotNil(t, sastScanManager.sastScannerResults) { @@ -57,7 +57,7 @@ func TestSastParseResults_ResultsContainIacViolations(t *testing.T) { // Act var err error - sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot) + sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, sastDocsUrlSuffix) // Assert if assert.NoError(t, err) && assert.NotNil(t, sastScanManager.sastScannerResults) { diff --git a/xray/commands/audit/jas/secrets/secretsscanner.go b/xray/commands/audit/jas/secrets/secretsscanner.go index eb383f6f4..aad7fc6a6 100644 --- a/xray/commands/audit/jas/secrets/secretsscanner.go +++ b/xray/commands/audit/jas/secrets/secretsscanner.go @@ -12,8 +12,9 @@ import ( ) const ( - secretsScanCommand = "sec" - secretsScannerType = "secrets-scan" + secretsScanCommand = "sec" + secretsScannerType = "secrets-scan" + secretsDocsUrlSuffix = "secrets" ) type SecretScanManager struct { @@ -59,7 +60,7 @@ func (ssm *SecretScanManager) Run(module jfrogappsconfig.Module) (err error) { if err = ssm.runAnalyzerManager(); err != nil { return } - workingDirRuns, err := jas.ReadJasScanRunsFromFile(ssm.scanner.ResultsFileName, module.SourceRoot) + workingDirRuns, err := jas.ReadJasScanRunsFromFile(ssm.scanner.ResultsFileName, module.SourceRoot, secretsDocsUrlSuffix) if err != nil { return } diff --git a/xray/commands/audit/jas/secrets/secretsscanner_test.go b/xray/commands/audit/jas/secrets/secretsscanner_test.go index bd9ec826d..1b2a053ec 100644 --- a/xray/commands/audit/jas/secrets/secretsscanner_test.go +++ b/xray/commands/audit/jas/secrets/secretsscanner_test.go @@ -66,7 +66,7 @@ func TestParseResults_EmptyResults(t *testing.T) { // Act var err error - secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot) + secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, secretsDocsUrlSuffix) // Assert if assert.NoError(t, err) && assert.NotNil(t, secretScanManager.secretsScannerResults) { @@ -89,7 +89,7 @@ func TestParseResults_ResultsContainSecrets(t *testing.T) { // Act var err error - secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot) + secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, secretsDocsUrlSuffix) // Assert if assert.NoError(t, err) && assert.NotNil(t, secretScanManager.secretsScannerResults) { diff --git a/xray/commands/audit/jasrunner.go b/xray/commands/audit/jasrunner.go index 76a59ff8e..c528fdadc 100644 --- a/xray/commands/audit/jasrunner.go +++ b/xray/commands/audit/jasrunner.go @@ -52,9 +52,6 @@ func runJasScannersAndSetResults(scanResults *utils.ExtendedScanResults, directD if err != nil { return } - if !utils.IsSastSupported() { - return - } if progress != nil { progress.SetHeadlineMsg("Running SAST scanning") } diff --git a/xray/utils/analyzermanager.go b/xray/utils/analyzermanager.go index 9866692de..cf894d660 100644 --- a/xray/utils/analyzermanager.go +++ b/xray/utils/analyzermanager.go @@ -3,7 +3,6 @@ package utils import ( "errors" "fmt" - "github.com/jfrog/gofrog/version" "os" "os/exec" "path" @@ -24,7 +23,6 @@ const ( ApplicabilityFeatureId = "contextual_analysis" AnalyzerManagerZipName = "analyzerManager.zip" defaultAnalyzerManagerVersion = "1.3.2.2019257" - minAnalyzerManagerVersionForSast = "1.3" analyzerManagerDownloadPath = "xsc-gen-exe-analyzer-manager-local/v1" analyzerManagerDirName = "analyzerManager" analyzerManagerExecutableName = "analyzerManager" @@ -150,10 +148,6 @@ func GetAnalyzerManagerVersion() string { return defaultAnalyzerManagerVersion } -func IsSastSupported() bool { - return version.NewVersion(GetAnalyzerManagerVersion()).AtLeast(minAnalyzerManagerVersionForSast) -} - func GetAnalyzerManagerDirAbsolutePath() (string, error) { jfrogDir, err := config.GetJfrogDependenciesPath() if err != nil { diff --git a/xray/utils/resultwriter.go b/xray/utils/resultwriter.go index 9a97a7e87..553f37388 100644 --- a/xray/utils/resultwriter.go +++ b/xray/utils/resultwriter.go @@ -25,6 +25,8 @@ const ( Json OutputFormat = "json" SimpleJson OutputFormat = "simple-json" Sarif OutputFormat = "sarif" + + BaseDocumentationURL = "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/" ) const MissingCveScore = "0" @@ -115,7 +117,11 @@ func (rw *ResultsWriter) PrintScanResults() error { case Json: return PrintJson(rw.results.getXrayScanResults()) case Sarif: - sarifFile, err := GenerateSarifContentFromResults(rw.results, rw.isMultipleRoots, rw.includeLicenses, false) + sarifReport, err := GenereateSarifReportFromResults(rw.results, rw.isMultipleRoots, rw.includeLicenses) + if err != nil { + return err + } + sarifFile, err := ConvertSarifReportToString(sarifReport) if err != nil { return err } @@ -153,9 +159,6 @@ func (rw *ResultsWriter) printScanResultsTables() (err error) { if err = PrintIacTable(rw.results.IacScanResults, rw.results.EntitledForJas); err != nil { return } - if !IsSastSupported() { - return - } return PrintSastTable(rw.results.SastScanResults, rw.results.EntitledForJas) } @@ -172,12 +175,12 @@ func printMessage(message string) { log.Output("💬" + message) } -func GenerateSarifContentFromResults(extendedResults *ExtendedScanResults, isMultipleRoots, includeLicenses, markdownOutput bool) (sarifStr string, err error) { - report, err := NewReport() +func GenereateSarifReportFromResults(extendedResults *ExtendedScanResults, isMultipleRoots, includeLicenses bool) (report *sarif.Report, err error) { + report, err = NewReport() if err != nil { return } - xrayRun, err := convertXrayResponsesToSarifRun(extendedResults, isMultipleRoots, includeLicenses, markdownOutput) + xrayRun, err := convertXrayResponsesToSarifRun(extendedResults, isMultipleRoots, includeLicenses) if err != nil { return } @@ -188,23 +191,26 @@ func GenerateSarifContentFromResults(extendedResults *ExtendedScanResults, isMul report.Runs = append(report.Runs, extendedResults.SecretsScanResults...) report.Runs = append(report.Runs, extendedResults.SastScanResults...) + return +} + +func ConvertSarifReportToString(report *sarif.Report) (sarifStr string, err error) { out, err := json.Marshal(report) if err != nil { return "", errorutils.CheckError(err) } - return clientUtils.IndentJson(out), nil } -func convertXrayResponsesToSarifRun(extendedResults *ExtendedScanResults, isMultipleRoots, includeLicenses, markdownOutput bool) (run *sarif.Run, err error) { +func convertXrayResponsesToSarifRun(extendedResults *ExtendedScanResults, isMultipleRoots, includeLicenses bool) (run *sarif.Run, err error) { xrayJson, err := convertXrayScanToSimpleJson(extendedResults, isMultipleRoots, includeLicenses, true) if err != nil { return } - xrayRun := sarif.NewRunWithInformationURI("JFrog Xray Sca", "https://jfrog.com/xray/") + xrayRun := sarif.NewRunWithInformationURI("JFrog Xray SCA", BaseDocumentationURL+"sca") xrayRun.Tool.Driver.Version = &extendedResults.XrayVersion if len(xrayJson.Vulnerabilities) > 0 || len(xrayJson.SecurityViolations) > 0 { - if err = extractXrayIssuesToSarifRun(xrayRun, xrayJson, markdownOutput); err != nil { + if err = extractXrayIssuesToSarifRun(xrayRun, xrayJson); err != nil { return } } @@ -212,20 +218,19 @@ func convertXrayResponsesToSarifRun(extendedResults *ExtendedScanResults, isMult return } -func extractXrayIssuesToSarifRun(run *sarif.Run, xrayJson formats.SimpleJsonResults, markdownOutput bool) error { +func extractXrayIssuesToSarifRun(run *sarif.Run, xrayJson formats.SimpleJsonResults) error { for _, vulnerability := range xrayJson.Vulnerabilities { if err := addXrayCveIssueToSarifRun( vulnerability.Cves, vulnerability.IssueId, vulnerability.Severity, - vulnerability.Technology.GetPackageDescriptor(), + vulnerability.Technology, vulnerability.Components, vulnerability.Applicable, vulnerability.ImpactedDependencyName, vulnerability.ImpactedDependencyVersion, vulnerability.Summary, vulnerability.FixedVersions, - markdownOutput, run, ); err != nil { return err @@ -236,14 +241,13 @@ func extractXrayIssuesToSarifRun(run *sarif.Run, xrayJson formats.SimpleJsonResu violation.Cves, violation.IssueId, violation.Severity, - violation.Technology.GetPackageDescriptor(), + violation.Technology, violation.Components, violation.Applicable, violation.ImpactedDependencyName, violation.ImpactedDependencyVersion, violation.Summary, violation.FixedVersions, - markdownOutput, run, ); err != nil { return err @@ -258,35 +262,66 @@ func extractXrayIssuesToSarifRun(run *sarif.Run, xrayJson formats.SimpleJsonResu return nil } -func addXrayCveIssueToSarifRun(cves []formats.CveRow, issueId, severity, file string, components []formats.ComponentRow, applicable, impactedDependencyName, impactedDependencyVersion, summary string, fixedVersions []string, markdownOutput bool, run *sarif.Run) error { +func addXrayCveIssueToSarifRun(cves []formats.CveRow, issueId, severity string, tech coreutils.Technology, components []formats.ComponentRow, applicable, impactedDependencyName, impactedDependencyVersion, summary string, fixedVersions []string, run *sarif.Run) error { maxCveScore, err := findMaxCVEScore(cves) if err != nil { return err } cveId := GetIssueIdentifier(cves, issueId) msg := getVulnerabilityOrViolationSarifHeadline(impactedDependencyName, impactedDependencyVersion, cveId) - location := sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri(file))) - + location, err := getXrayIssueLocationIfValidExists(tech, run) + if err != nil { + return err + } if rule, isNewRule := addResultToSarifRun(cveId, msg, severity, location, run); isNewRule { cveRuleProperties := sarif.NewPropertyBag() if maxCveScore != MissingCveScore { cveRuleProperties.Add("security-severity", maxCveScore) } rule.WithProperties(cveRuleProperties.Properties) - if markdownOutput { - formattedDirectDependencies, err := getDirectDependenciesFormatted(components) - if err != nil { - return err - } - markdownDescription := getSarifTableDescription(formattedDirectDependencies, maxCveScore, applicable, fixedVersions) + "\n" - rule.WithMarkdownHelp(markdownDescription) - } else { - rule.WithDescription(summary) + formattedDirectDependencies, err := getDirectDependenciesFormatted(components) + if err != nil { + return err } + markdownDescription := getSarifTableDescription(formattedDirectDependencies, maxCveScore, applicable, fixedVersions) + "\n" + rule.WithHelp(&sarif.MultiformatMessageString{ + Text: &summary, + Markdown: &markdownDescription, + }) } return nil } +func getDescriptorFullPath(tech coreutils.Technology, run *sarif.Run) (string, error) { + descriptors := tech.GetPackageDescriptor() + if len(descriptors) == 1 { + // Generate the full path + return GetFullLocationFileName(strings.TrimSpace(descriptors[0]), run.Invocations), nil + } + for _, descriptor := range descriptors { + // If multiple options return first to match + absolutePath := GetFullLocationFileName(strings.TrimSpace(descriptor), run.Invocations) + if exists, err := fileutils.IsFileExists(absolutePath, false); err != nil { + return "", err + } else if exists { + return absolutePath, nil + } + } + return "", nil +} + +// Get the descriptor location with the Xray issues if exists. +func getXrayIssueLocationIfValidExists(tech coreutils.Technology, run *sarif.Run) (location *sarif.Location, err error) { + descriptorPath, err := getDescriptorFullPath(tech, run) + if err != nil { + return + } + if strings.TrimSpace(descriptorPath) == "" { + return + } + return sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://" + descriptorPath))), nil +} + func addResultToSarifRun(issueId, msg, severity string, location *sarif.Location, run *sarif.Run) (rule *sarif.ReportingDescriptor, isNewRule bool) { if rule, _ = run.GetRuleById(issueId); rule == nil { isNewRule = true diff --git a/xray/utils/resultwriter_test.go b/xray/utils/resultwriter_test.go index 57458ea34..a67dfe86c 100644 --- a/xray/utils/resultwriter_test.go +++ b/xray/utils/resultwriter_test.go @@ -1,9 +1,14 @@ package utils import ( + "os" + "path/filepath" "testing" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/utils/tests" "github.com/jfrog/jfrog-cli-core/v2/xray/formats" + "github.com/owenrumney/go-sarif/v2/sarif" "github.com/stretchr/testify/assert" ) @@ -134,3 +139,57 @@ func TestFindMaxCVEScore(t *testing.T) { }) } } + +func TestGetXrayIssueLocationIfValidExists(t *testing.T) { + testDir, cleanup := tests.CreateTempDirWithCallbackAndAssert(t) + defer cleanup() + invocation := sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(testDir)) + file, err := os.Create(filepath.Join(testDir, "go.mod")) + assert.NoError(t, err) + assert.NotNil(t, file) + defer func() { assert.NoError(t, file.Close()) }() + file2, err := os.Create(filepath.Join(testDir, "build.gradle.kts")) + assert.NoError(t, err) + assert.NotNil(t, file2) + defer func() { assert.NoError(t, file2.Close()) }() + + testCases := []struct { + name string + tech coreutils.Technology + run *sarif.Run + expectedOutput *sarif.Location + }{ + { + name: "No descriptor information", + tech: coreutils.Pip, + run: CreateRunWithDummyResults().WithInvocations([]*sarif.Invocation{invocation}), + expectedOutput: nil, + }, + { + name: "One descriptor information", + tech: coreutils.Go, + run: CreateRunWithDummyResults().WithInvocations([]*sarif.Invocation{invocation}), + expectedOutput: sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://" + filepath.Join(testDir, "go.mod")))), + }, + { + name: "One descriptor information - no invocation", + tech: coreutils.Go, + run: CreateRunWithDummyResults(), + expectedOutput: sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://go.mod"))), + }, + { + name: "Multiple descriptor information", + tech: coreutils.Gradle, + run: CreateRunWithDummyResults().WithInvocations([]*sarif.Invocation{invocation}), + expectedOutput: sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://" + filepath.Join(testDir, "build.gradle.kts")))), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output, err := getXrayIssueLocationIfValidExists(tc.tech, tc.run) + if assert.NoError(t, err) { + assert.Equal(t, tc.expectedOutput, output) + } + }) + } +} diff --git a/xray/utils/sarifutils.go b/xray/utils/sarifutils.go index d0a959bfe..0da577140 100644 --- a/xray/utils/sarifutils.go +++ b/xray/utils/sarifutils.go @@ -147,7 +147,6 @@ func GetRelativeLocationFileName(location *sarif.Location, invocations []*sarif. if len(invocations) > 0 { wd = GetInvocationWorkingDirectory(invocations[0]) } - GetLocationFileName(location) filePath := GetLocationFileName(location) if filePath != "" { return ExtractRelativePath(filePath, wd) @@ -155,8 +154,15 @@ func GetRelativeLocationFileName(location *sarif.Location, invocations []*sarif. return "" } +func GetFullLocationFileName(relative string, invocations []*sarif.Invocation) string { + if len(invocations) == 0 { + return relative + } + return filepath.Join(GetInvocationWorkingDirectory(invocations[0]), relative) +} + func SetLocationFileName(location *sarif.Location, fileName string) { - if location != nil && location.PhysicalLocation != nil && location.PhysicalLocation.Region != nil && location.PhysicalLocation.Region.Snippet != nil { + if location != nil && location.PhysicalLocation != nil && location.PhysicalLocation.ArtifactLocation != nil { location.PhysicalLocation.ArtifactLocation.URI = &fileName } } @@ -259,64 +265,3 @@ func GetInvocationWorkingDirectory(invocation *sarif.Invocation) string { } return "" } - -func CreateRunWithDummyResults(results ...*sarif.Result) *sarif.Run { - run := sarif.NewRunWithInformationURI("", "") - for _, result := range results { - if result.RuleID != nil { - run.AddRule(*result.RuleID) - } - run.AddResult(result) - } - return run -} - -func CreateResultWithLocations(msg, ruleId, level string, locations ...*sarif.Location) *sarif.Result { - return &sarif.Result{ - Message: *sarif.NewTextMessage(msg), - Locations: locations, - Level: &level, - RuleID: &ruleId, - } -} - -func CreateLocation(fileName string, startLine, startCol, endLine, endCol int, snippet string) *sarif.Location { - return &sarif.Location{ - PhysicalLocation: &sarif.PhysicalLocation{ - ArtifactLocation: &sarif.ArtifactLocation{URI: &fileName}, - Region: &sarif.Region{ - StartLine: &startLine, - StartColumn: &startCol, - EndLine: &endLine, - EndColumn: &endCol, - Snippet: &sarif.ArtifactContent{Text: &snippet}}}, - } -} - -func CreateDummyPassingResult(ruleId string) *sarif.Result { - kind := "pass" - return &sarif.Result{ - Kind: &kind, - RuleID: &ruleId, - } -} - -func CreateResultWithOneLocation(fileName string, startLine, startCol, endLine, endCol int, snippet, ruleId, level string) *sarif.Result { - return CreateResultWithLocations("", ruleId, level, CreateLocation(fileName, startLine, startCol, endLine, endCol, snippet)) -} - -func CreateCodeFlow(threadFlows ...*sarif.ThreadFlow) *sarif.CodeFlow { - flow := sarif.NewCodeFlow() - for _, threadFlow := range threadFlows { - flow.AddThreadFlow(threadFlow) - } - return flow -} - -func CreateThreadFlow(locations ...*sarif.Location) *sarif.ThreadFlow { - stackStrace := sarif.NewThreadFlow() - for _, location := range locations { - stackStrace.AddLocation(sarif.NewThreadFlowLocation().WithLocation(location)) - } - return stackStrace -} diff --git a/xray/utils/sarifutils_test.go b/xray/utils/sarifutils_test.go index 20a92de41..33358fe7a 100644 --- a/xray/utils/sarifutils_test.go +++ b/xray/utils/sarifutils_test.go @@ -1,6 +1,7 @@ package utils import ( + "path/filepath" "testing" "github.com/owenrumney/go-sarif/v2/sarif" @@ -294,6 +295,32 @@ func TestGetRelativeLocationFileName(t *testing.T) { } } +func TestGetFullLocationFileName(t *testing.T) { + tests := []struct { + file string + invocations []*sarif.Invocation + expectedOutput string + }{ + { + file: filepath.Join("root", "someDir", "another", "file"), + invocations: []*sarif.Invocation{}, + expectedOutput: filepath.Join("root", "someDir", "another", "file"), + }, + { + file: filepath.Join("another", "file"), + invocations: []*sarif.Invocation{ + {WorkingDirectory: sarif.NewSimpleArtifactLocation(filepath.Join("root", "someDir"))}, + {WorkingDirectory: sarif.NewSimpleArtifactLocation(filepath.Join("not", "relevant"))}, + }, + expectedOutput: filepath.Join("root", "someDir", "another", "file"), + }, + } + + for _, test := range tests { + assert.Equal(t, test.expectedOutput, GetFullLocationFileName(test.file, test.invocations)) + } +} + func TestSetLocationFileName(t *testing.T) { tests := []struct { location *sarif.Location diff --git a/xray/utils/test_sarifutils.go b/xray/utils/test_sarifutils.go new file mode 100644 index 000000000..5034b1eb6 --- /dev/null +++ b/xray/utils/test_sarifutils.go @@ -0,0 +1,64 @@ +package utils + +import "github.com/owenrumney/go-sarif/v2/sarif" + +func CreateRunWithDummyResults(results ...*sarif.Result) *sarif.Run { + run := sarif.NewRunWithInformationURI("", "") + for _, result := range results { + if result.RuleID != nil { + run.AddRule(*result.RuleID) + } + run.AddResult(result) + } + return run +} + +func CreateResultWithLocations(msg, ruleId, level string, locations ...*sarif.Location) *sarif.Result { + return &sarif.Result{ + Message: *sarif.NewTextMessage(msg), + Locations: locations, + Level: &level, + RuleID: &ruleId, + } +} + +func CreateLocation(fileName string, startLine, startCol, endLine, endCol int, snippet string) *sarif.Location { + return &sarif.Location{ + PhysicalLocation: &sarif.PhysicalLocation{ + ArtifactLocation: &sarif.ArtifactLocation{URI: &fileName}, + Region: &sarif.Region{ + StartLine: &startLine, + StartColumn: &startCol, + EndLine: &endLine, + EndColumn: &endCol, + Snippet: &sarif.ArtifactContent{Text: &snippet}}}, + } +} + +func CreateDummyPassingResult(ruleId string) *sarif.Result { + kind := "pass" + return &sarif.Result{ + Kind: &kind, + RuleID: &ruleId, + } +} + +func CreateResultWithOneLocation(fileName string, startLine, startCol, endLine, endCol int, snippet, ruleId, level string) *sarif.Result { + return CreateResultWithLocations("", ruleId, level, CreateLocation(fileName, startLine, startCol, endLine, endCol, snippet)) +} + +func CreateCodeFlow(threadFlows ...*sarif.ThreadFlow) *sarif.CodeFlow { + flow := sarif.NewCodeFlow() + for _, threadFlow := range threadFlows { + flow.AddThreadFlow(threadFlow) + } + return flow +} + +func CreateThreadFlow(locations ...*sarif.Location) *sarif.ThreadFlow { + stackStrace := sarif.NewThreadFlow() + for _, location := range locations { + stackStrace.AddLocation(sarif.NewThreadFlowLocation().WithLocation(location)) + } + return stackStrace +}