From 54ee983f76b2e32cd02bd54d729c35f342de2c50 Mon Sep 17 00:00:00 2001 From: Assaf Attias <49212512+attiasas@users.noreply.github.com> Date: Sun, 3 Sep 2023 11:11:37 +0300 Subject: [PATCH] Add SAST advance scan (#919) --- xray/audit/jas/applicabilitymanager.go | 10 +- xray/audit/jas/applicabilitymanager_test.go | 188 ++-- xray/audit/jas/iacscanner.go | 18 +- xray/audit/jas/iacscanner_test.go | 66 +- xray/audit/jas/jasmanager.go | 56 +- xray/audit/jas/jasmanager_test.go | 10 + xray/audit/jas/sastscanner.go | 54 ++ xray/audit/jas/sastscanner_test.go | 59 ++ xray/audit/jas/secretsscanner.go | 17 +- xray/audit/jas/secretsscanner_test.go | 85 +- xray/commands/audit/generic/generic.go | 2 +- .../sast-scan/contains-sast-violations.sarif | 907 ++++++++++++++++++ .../testdata/sast-scan/no-violations.sarif | 28 + xray/formats/conversion.go | 16 +- xray/formats/simplejsonapi.go | 20 +- xray/formats/table.go | 7 + xray/utils/analyzermanager.go | 109 +-- xray/utils/analyzermanager_test.go | 2 +- xray/utils/resultstable.go | 108 ++- xray/utils/resultwriter.go | 146 ++- xray/utils/resultwriter_test.go | 77 +- xray/utils/sarifutils.go | 130 +++ 22 files changed, 1670 insertions(+), 445 deletions(-) create mode 100644 xray/audit/jas/sastscanner.go create mode 100644 xray/audit/jas/sastscanner_test.go create mode 100644 xray/commands/testdata/sast-scan/contains-sast-violations.sarif create mode 100644 xray/commands/testdata/sast-scan/no-violations.sarif create mode 100644 xray/utils/sarifutils.go diff --git a/xray/audit/jas/applicabilitymanager.go b/xray/audit/jas/applicabilitymanager.go index 4c99df9cc..373d15d6d 100644 --- a/xray/audit/jas/applicabilitymanager.go +++ b/xray/audit/jas/applicabilitymanager.go @@ -1,6 +1,9 @@ package jas import ( + "path/filepath" + "strings" + "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" @@ -10,7 +13,6 @@ import ( "github.com/owenrumney/go-sarif/v2/sarif" "golang.org/x/exp/maps" "golang.org/x/exp/slices" - "strings" ) const ( @@ -109,7 +111,9 @@ func (a *ApplicabilityScanManager) Run(wd string) (err error) { return } var workingDirResults map[string]string - workingDirResults, err = a.getScanResults() + if workingDirResults, err = a.getScanResults(); err != nil { + return + } for cve, result := range workingDirResults { a.applicabilityScanResults[cve] = result } @@ -156,7 +160,7 @@ func (a *ApplicabilityScanManager) createConfigFile(workingDir string) error { // Runs the analyzerManager app and returns a boolean to indicate whether the user is entitled for // advance security feature func (a *ApplicabilityScanManager) runAnalyzerManager() error { - return a.scanner.analyzerManager.Exec(a.scanner.configFileName, applicabilityScanCommand, a.scanner.serverDetails) + return a.scanner.analyzerManager.Exec(a.scanner.configFileName, applicabilityScanCommand, filepath.Dir(a.scanner.analyzerManager.AnalyzerManagerFullPath), a.scanner.serverDetails) } func (a *ApplicabilityScanManager) getScanResults() (map[string]string, error) { diff --git a/xray/audit/jas/applicabilitymanager_test.go b/xray/audit/jas/applicabilitymanager_test.go index 3c8ffc5a8..079da7893 100644 --- a/xray/audit/jas/applicabilitymanager_test.go +++ b/xray/audit/jas/applicabilitymanager_test.go @@ -12,48 +12,37 @@ import ( ) func TestNewApplicabilityScanManager_InputIsValid(t *testing.T) { + scanner, cleanUp := initJasTest(t) + defer cleanUp() // Act - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, mockDirectDependencies, scanner) // Assert - assert.NotEmpty(t, applicabilityManager) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - assert.Equal(t, applicabilityManager.directDependenciesCves.Size(), 5) + if assert.NotNil(t, applicabilityManager) { + assert.NotEmpty(t, applicabilityManager.scanner.configFileName) + assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) + assert.Equal(t, 5, applicabilityManager.directDependenciesCves.Size()) + } } func TestNewApplicabilityScanManager_DependencyTreeDoesntExist(t *testing.T) { + scanner, cleanUp := initJasTest(t) + defer cleanUp() // Act - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, nil, scanner) // Assert - assert.NotEmpty(t, applicabilityManager) - assert.NotNil(t, applicabilityManager.scanner.scannerDirCleanupFunc) - assert.Len(t, applicabilityManager.scanner.workingDirs, 1) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - assert.Equal(t, applicabilityManager.directDependenciesCves.Size(), 0) + if assert.NotNil(t, applicabilityManager) { + assert.NotNil(t, applicabilityManager.scanner.scannerDirCleanupFunc) + assert.Len(t, applicabilityManager.scanner.workingDirs, 1) + assert.NotEmpty(t, applicabilityManager.scanner.configFileName) + assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) + assert.Equal(t, applicabilityManager.directDependenciesCves.Size(), 0) + } } func TestNewApplicabilityScanManager_NoDirectDependenciesInScan(t *testing.T) { // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) var noDirectDependenciesResults = []services.ScanResponse{ { ScanId: "scanId_1", @@ -75,47 +64,36 @@ func TestNewApplicabilityScanManager_NoDirectDependenciesInScan(t *testing.T) { fakeBasicXrayResults[0].Violations[0].Components["issueId_2_non_direct_dependency"] = services.Component{} // Act - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t) + defer cleanUp() applicabilityManager := newApplicabilityScanManager(noDirectDependenciesResults, mockDirectDependencies, scanner) // Assert - assert.NotEmpty(t, applicabilityManager) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - // Non-direct dependencies should not be added - assert.Equal(t, 0, applicabilityManager.directDependenciesCves.Size()) + if assert.NotNil(t, applicabilityManager) { + assert.NotEmpty(t, applicabilityManager.scanner.configFileName) + assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) + // Non-direct dependencies should not be added + assert.Equal(t, 0, applicabilityManager.directDependenciesCves.Size()) + } } func TestNewApplicabilityScanManager_MultipleDependencyTrees(t *testing.T) { // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - + scanner, cleanUp := initJasTest(t) + defer cleanUp() // Act - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, mockMultiRootDirectDependencies, scanner) // Assert - assert.NotEmpty(t, applicabilityManager) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - assert.Equal(t, 5, applicabilityManager.directDependenciesCves.Size()) + if assert.NotNil(t, applicabilityManager) { + assert.NotEmpty(t, applicabilityManager.scanner.configFileName) + assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) + assert.Equal(t, 5, applicabilityManager.directDependenciesCves.Size()) + } } func TestNewApplicabilityScanManager_ViolationsDontExistInResults(t *testing.T) { // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) noViolationScanResponse := []services.ScanResponse{ { ScanId: "scanId_1", @@ -126,28 +104,22 @@ func TestNewApplicabilityScanManager_ViolationsDontExistInResults(t *testing.T) }, }, } + scanner, cleanUp := initJasTest(t) + defer cleanUp() // Act - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() applicabilityManager := newApplicabilityScanManager(noViolationScanResponse, mockDirectDependencies, scanner) // Assert - assert.NoError(t, err) - assert.NotEmpty(t, applicabilityManager) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - assert.Equal(t, 3, applicabilityManager.directDependenciesCves.Size()) + if assert.NotNil(t, applicabilityManager) { + assert.NotEmpty(t, applicabilityManager.scanner.configFileName) + assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) + assert.Equal(t, 3, applicabilityManager.directDependenciesCves.Size()) + } } func TestNewApplicabilityScanManager_VulnerabilitiesDontExist(t *testing.T) { // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) noVulnerabilitiesScanResponse := []services.ScanResponse{ { ScanId: "scanId_1", @@ -158,33 +130,24 @@ func TestNewApplicabilityScanManager_VulnerabilitiesDontExist(t *testing.T) { }, }, } + scanner, cleanUp := initJasTest(t) + defer cleanUp() // Act - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() applicabilityManager := newApplicabilityScanManager(noVulnerabilitiesScanResponse, mockDirectDependencies, scanner) // Assert - assert.NotEmpty(t, applicabilityManager) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - assert.Equal(t, 2, applicabilityManager.directDependenciesCves.Size()) + if assert.NotNil(t, applicabilityManager) { + assert.NotEmpty(t, applicabilityManager.scanner.configFileName) + assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) + assert.Equal(t, 2, applicabilityManager.directDependenciesCves.Size()) + } } func TestApplicabilityScanManager_ShouldRun_TechnologiesNotEligibleForScan(t *testing.T) { - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t) + defer cleanUp() + results, err := getApplicabilityScanResults(fakeBasicXrayResults, mockDirectDependencies, []coreutils.Technology{coreutils.Nuget, coreutils.Go}, scanner) @@ -195,16 +158,11 @@ func TestApplicabilityScanManager_ShouldRun_TechnologiesNotEligibleForScan(t *te func TestApplicabilityScanManager_ShouldRun_ScanResultsAreEmpty(t *testing.T) { // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t) + defer cleanUp() + applicabilityManager := newApplicabilityScanManager(nil, mockDirectDependencies, scanner) - assert.NoError(t, err) + // Assert eligible := applicabilityManager.shouldRunApplicabilityScan([]coreutils.Technology{coreutils.Npm}) assert.False(t, eligible) @@ -284,14 +242,9 @@ func TestExtractXrayDirectVulnerabilities(t *testing.T) { func TestCreateConfigFile_VerifyFileWasCreated(t *testing.T) { // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t) + defer cleanUp() + applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, scanner) currWd, err := coreutils.GetWorkingDirectory() @@ -313,14 +266,9 @@ func TestCreateConfigFile_VerifyFileWasCreated(t *testing.T) { func TestParseResults_EmptyResults_AllCvesShouldGetUnknown(t *testing.T) { // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t) + defer cleanUp() + applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, mockDirectDependencies, scanner) applicabilityManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "applicability-scan", "empty-results.sarif") @@ -337,14 +285,8 @@ func TestParseResults_EmptyResults_AllCvesShouldGetUnknown(t *testing.T) { func TestParseResults_ApplicableCveExist(t *testing.T) { // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t) + defer cleanUp() applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, mockDirectDependencies, scanner) applicabilityManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "applicability-scan", "applicable-cve-results.sarif") @@ -360,14 +302,8 @@ func TestParseResults_ApplicableCveExist(t *testing.T) { func TestParseResults_AllCvesNotApplicable(t *testing.T) { // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t) + defer cleanUp() applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, mockDirectDependencies, scanner) applicabilityManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "applicability-scan", "no-applicable-cves-results.sarif") diff --git a/xray/audit/jas/iacscanner.go b/xray/audit/jas/iacscanner.go index 629cd384f..56c9dad96 100644 --- a/xray/audit/jas/iacscanner.go +++ b/xray/audit/jas/iacscanner.go @@ -1,6 +1,8 @@ package jas import ( + "path/filepath" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/utils/log" ) @@ -11,7 +13,7 @@ const ( ) type IacScanManager struct { - iacScannerResults []utils.IacOrSecretResult + iacScannerResults []utils.SourceCodeScanResult scanner *AdvancedSecurityScanner } @@ -20,10 +22,10 @@ type IacScanManager struct { // Running the analyzer manager executable. // Parsing the analyzer manager results. // Return values: -// []utils.IacOrSecretResult: a list of the iac violations that were found. +// []utils.SourceCodeScanResult: a list of the iac violations that were found. // bool: true if the user is entitled to iac scan, false otherwise. // error: An error object (if any). -func getIacScanResults(scanner *AdvancedSecurityScanner) (results []utils.IacOrSecretResult, err error) { +func getIacScanResults(scanner *AdvancedSecurityScanner) (results []utils.SourceCodeScanResult, err error) { iacScanManager := newIacScanManager(scanner) log.Info("Running IaC scanning...") if err = iacScanManager.scanner.Run(iacScanManager); err != nil { @@ -39,7 +41,7 @@ func getIacScanResults(scanner *AdvancedSecurityScanner) (results []utils.IacOrS func newIacScanManager(scanner *AdvancedSecurityScanner) (manager *IacScanManager) { return &IacScanManager{ - iacScannerResults: []utils.IacOrSecretResult{}, + iacScannerResults: []utils.SourceCodeScanResult{}, scanner: scanner, } } @@ -52,8 +54,10 @@ func (iac *IacScanManager) Run(wd string) (err error) { if err = iac.runAnalyzerManager(); err != nil { return } - var workingDirResults []utils.IacOrSecretResult - workingDirResults, err = getIacOrSecretsScanResults(scanner.resultsFileName, wd, false) + var workingDirResults []utils.SourceCodeScanResult + if workingDirResults, err = getSourceCodeScanResults(scanner.resultsFileName, wd, utils.IaC); err != nil { + return + } iac.iacScannerResults = append(iac.iacScannerResults, workingDirResults...) return } @@ -84,5 +88,5 @@ func (iac *IacScanManager) createConfigFile(currentWd string) error { } func (iac *IacScanManager) runAnalyzerManager() error { - return iac.scanner.analyzerManager.Exec(iac.scanner.configFileName, iacScanCommand, iac.scanner.serverDetails) + return iac.scanner.analyzerManager.Exec(iac.scanner.configFileName, iacScanCommand, filepath.Dir(iac.scanner.analyzerManager.AnalyzerManagerFullPath), iac.scanner.serverDetails) } diff --git a/xray/audit/jas/iacscanner_test.go b/xray/audit/jas/iacscanner_test.go index 430b75fdb..42a5d9d3c 100644 --- a/xray/audit/jas/iacscanner_test.go +++ b/xray/audit/jas/iacscanner_test.go @@ -1,44 +1,34 @@ package jas import ( - rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/stretchr/testify/assert" "os" "path/filepath" "testing" + + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/stretchr/testify/assert" ) func TestNewIacScanManager(t *testing.T) { + scanner, cleanUp := initJasTest(t, "currentDir") + defer cleanUp() // Act - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner([]string{"currentDir"}, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() iacScanManager := newIacScanManager(scanner) // Assert - assert.NoError(t, err) - assert.NotEmpty(t, iacScanManager) - assert.NotEmpty(t, iacScanManager.scanner.configFileName) - assert.NotEmpty(t, iacScanManager.scanner.resultsFileName) - assert.NotEmpty(t, iacScanManager.scanner.workingDirs) - assert.Equal(t, &fakeServerDetails, iacScanManager.scanner.serverDetails) + if assert.NotNil(t, iacScanManager) { + assert.NotEmpty(t, iacScanManager.scanner.configFileName) + assert.NotEmpty(t, iacScanManager.scanner.resultsFileName) + assert.NotEmpty(t, iacScanManager.scanner.workingDirs) + assert.Equal(t, &fakeServerDetails, iacScanManager.scanner.serverDetails) + } } func TestIacScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner([]string{"currentDir"}, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t, "currentDir") + defer cleanUp() + iacScanManager := newIacScanManager(scanner) currWd, err := coreutils.GetWorkingDirectory() @@ -58,20 +48,16 @@ func TestIacScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { } func TestIacParseResults_EmptyResults(t *testing.T) { + scanner, cleanUp := initJasTest(t) + defer cleanUp() + // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() iacScanManager := newIacScanManager(scanner) iacScanManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "iac-scan", "no-violations.sarif") // Act - iacScanManager.iacScannerResults, err = getIacOrSecretsScanResults(iacScanManager.scanner.resultsFileName, scanner.workingDirs[0], false) + var err error + iacScanManager.iacScannerResults, err = getSourceCodeScanResults(iacScanManager.scanner.resultsFileName, scanner.workingDirs[0], utils.IaC) // Assert assert.NoError(t, err) @@ -79,19 +65,15 @@ func TestIacParseResults_EmptyResults(t *testing.T) { } func TestIacParseResults_ResultsContainIacViolations(t *testing.T) { + scanner, cleanUp := initJasTest(t) + defer cleanUp() // Arrange - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() iacScanManager := newIacScanManager(scanner) iacScanManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "iac-scan", "contains-iac-violations.sarif") // Act - iacScanManager.iacScannerResults, err = getIacOrSecretsScanResults(iacScanManager.scanner.resultsFileName, scanner.workingDirs[0], false) + var err error + iacScanManager.iacScannerResults, err = getSourceCodeScanResults(iacScanManager.scanner.resultsFileName, scanner.workingDirs[0], utils.IaC) // Assert assert.NoError(t, err) diff --git a/xray/audit/jas/jasmanager.go b/xray/audit/jas/jasmanager.go index c3304ecdf..3b191d0cb 100644 --- a/xray/audit/jas/jasmanager.go +++ b/xray/audit/jas/jasmanager.go @@ -2,6 +2,10 @@ package jas import ( "errors" + "os" + "path/filepath" + + "github.com/jfrog/gofrog/version" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" @@ -10,8 +14,6 @@ import ( "github.com/jfrog/jfrog-client-go/utils/log" "github.com/owenrumney/go-sarif/v2/sarif" "gopkg.in/yaml.v3" - "os" - "path/filepath" ) var ( @@ -96,6 +98,16 @@ func RunScannersAndSetResults(scanResults *utils.ExtendedScanResults, directDepe progress.SetHeadlineMsg("Running IaC scanning") } scanResults.IacScanResults, err = getIacScanResults(scanner) + if err != nil { + return + } + if !version.NewVersion(utils.AnalyzerManagerVersion).AtLeast(utils.MinAnalyzerManagerVersionForSast) { + return + } + if progress != nil { + progress.SetHeadlineMsg("Running SAST scanning") + } + scanResults.SastResults, err = getSastScanResults(scanner) return } @@ -119,36 +131,42 @@ func deleteJasProcessFiles(configFile string, resultFile string) error { return errorutils.CheckError(err) } -func getIacOrSecretsScanResults(resultsFileName, workingDir string, isSecret bool) ([]utils.IacOrSecretResult, error) { +func getSourceCodeScanResults(resultsFileName, workingDir string, scanType utils.JasScanType) ([]utils.SourceCodeScanResult, error) { + // Read Sarif format results generated from the Jas scanner report, err := sarif.Open(resultsFileName) if errorutils.CheckError(err) != nil { return nil, err } - var results []*sarif.Result + var sarifResults []*sarif.Result if len(report.Runs) > 0 { - results = report.Runs[0].Results + // Jas scanners returns results in a single run entry + sarifResults = report.Runs[0].Results } + return convertSarifResultsToSourceCodeScanResults(sarifResults, workingDir, scanType), nil +} - var iacOrSecretResults []utils.IacOrSecretResult - for _, result := range results { +func convertSarifResultsToSourceCodeScanResults(sarifResults []*sarif.Result, workingDir string, scanType utils.JasScanType) []utils.SourceCodeScanResult { + var sourceCodeScanResults []utils.SourceCodeScanResult + for _, sarifResult := range sarifResults { // Describes a request to “suppress” a result (to exclude it from result lists) - if len(result.Suppressions) > 0 { + if len(sarifResult.Suppressions) > 0 { continue } - text := *result.Message.Text - if isSecret { - text = hideSecret(*result.Locations[0].PhysicalLocation.Region.Snippet.Text) + // Convert + sourceCodeScanResult := utils.IsSarifResultExistsInSourceCodeScanResults(sarifResult, workingDir, &sourceCodeScanResults) + if sourceCodeScanResult == nil { + sourceCodeScanResult = utils.ConvertSarifResultToSourceCodeScanResult(sarifResult, workingDir, &sourceCodeScanResults) + sourceCodeScanResults = append(sourceCodeScanResults, *sourceCodeScanResult) + } + // Set specific Jas scan attributes + if scanType == utils.Secrets { + sourceCodeScanResult.Text = hideSecret(utils.GetResultLocationSnippet(sarifResult.Locations[0])) } - newResult := utils.IacOrSecretResult{ - Severity: utils.GetResultSeverity(result), - File: utils.ExtractRelativePath(utils.GetResultFileName(result), workingDir), - LineColumn: utils.GetResultLocationInFile(result), - Text: text, - Type: *result.RuleID, + if scanType == utils.Sast { + sourceCodeScanResult.CodeFlow = append(sourceCodeScanResult.CodeFlow, utils.GetResultCodeFlows(sarifResult, workingDir)...) } - iacOrSecretResults = append(iacOrSecretResults, newResult) } - return iacOrSecretResults, nil + return sourceCodeScanResults } func createScannersConfigFile(fileName string, fileContent interface{}) error { diff --git a/xray/audit/jas/jasmanager_test.go b/xray/audit/jas/jasmanager_test.go index 960c1e9d3..405930553 100644 --- a/xray/audit/jas/jasmanager_test.go +++ b/xray/audit/jas/jasmanager_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" @@ -37,6 +38,15 @@ var fakeServerDetails = config.ServerDetails{ User: "user", } +func initJasTest(t *testing.T, workingDirs ...string) (*AdvancedSecurityScanner, func()) { + assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) + scanner, err := NewAdvancedSecurityScanner(workingDirs, &fakeServerDetails) + assert.NoError(t, err) + return scanner, func() { + assert.NoError(t, scanner.scannerDirCleanupFunc()) + } +} + func TestGetExtendedScanResults_AnalyzerManagerDoesntExist(t *testing.T) { tmpDir, err := fileutils.CreateTempDir() defer func() { diff --git a/xray/audit/jas/sastscanner.go b/xray/audit/jas/sastscanner.go new file mode 100644 index 000000000..62116b4ca --- /dev/null +++ b/xray/audit/jas/sastscanner.go @@ -0,0 +1,54 @@ +package jas + +import ( + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +const ( + sastScanCommand = "zd" + sastScannerType = "analyze-codebase" +) + +type SastScanManager struct { + sastScannerResults []utils.SourceCodeScanResult + scanner *AdvancedSecurityScanner +} + +func newSastScanManager(scanner *AdvancedSecurityScanner) (manager *SastScanManager) { + return &SastScanManager{ + sastScannerResults: []utils.SourceCodeScanResult{}, + scanner: scanner, + } +} + +func (zd *SastScanManager) Run(wd string) (err error) { + scanner := zd.scanner + if err = zd.runAnalyzerManager(wd); err != nil { + return + } + var workingDirResults []utils.SourceCodeScanResult + if workingDirResults, err = getSourceCodeScanResults(scanner.resultsFileName, wd, utils.Sast); err != nil { + return + } + zd.sastScannerResults = append(zd.sastScannerResults, workingDirResults...) + return +} + +func (zd *SastScanManager) runAnalyzerManager(wd string) error { + return zd.scanner.analyzerManager.Exec(zd.scanner.resultsFileName, sastScanCommand, wd, zd.scanner.serverDetails) +} + +func getSastScanResults(scanner *AdvancedSecurityScanner) (results []utils.SourceCodeScanResult, err error) { + sastScanManager := newSastScanManager(scanner) + log.Info("Running SAST scanning...") + if err = sastScanManager.scanner.Run(sastScanManager); err != nil { + err = utils.ParseAnalyzerManagerError(utils.Sast, err) + return + } + if len(sastScanManager.sastScannerResults) > 0 { + log.Info("Found", len(sastScanManager.sastScannerResults), "SAST vulnerabilities") + } + results = sastScanManager.sastScannerResults + return +} diff --git a/xray/audit/jas/sastscanner_test.go b/xray/audit/jas/sastscanner_test.go new file mode 100644 index 000000000..f309f84ff --- /dev/null +++ b/xray/audit/jas/sastscanner_test.go @@ -0,0 +1,59 @@ +package jas + +import ( + "path/filepath" + "testing" + + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/stretchr/testify/assert" +) + +func TestNewSastScanManager(t *testing.T) { + scanner, cleanUp := initJasTest(t, "currentDir") + defer cleanUp() + // Act + sastScanManager := newSastScanManager(scanner) + + // Assert + if assert.NotNil(t, sastScanManager) { + assert.NotEmpty(t, sastScanManager.scanner.configFileName) + assert.NotEmpty(t, sastScanManager.scanner.resultsFileName) + assert.NotEmpty(t, sastScanManager.scanner.workingDirs) + assert.Equal(t, &fakeServerDetails, sastScanManager.scanner.serverDetails) + } +} + +func TestSastParseResults_EmptyResults(t *testing.T) { + scanner, cleanUp := initJasTest(t) + defer cleanUp() + + // Arrange + sastScanManager := newSastScanManager(scanner) + sastScanManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "sast-scan", "no-violations.sarif") + + // Act + var err error + sastScanManager.sastScannerResults, err = getSourceCodeScanResults(sastScanManager.scanner.resultsFileName, scanner.workingDirs[0], utils.Sast) + + // Assert + assert.NoError(t, err) + assert.Empty(t, sastScanManager.sastScannerResults) +} + +func TestSastParseResults_ResultsContainIacViolations(t *testing.T) { + scanner, cleanUp := initJasTest(t) + defer cleanUp() + // Arrange + sastScanManager := newSastScanManager(scanner) + sastScanManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "sast-scan", "contains-sast-violations.sarif") + + // Act + var err error + sastScanManager.sastScannerResults, err = getSourceCodeScanResults(sastScanManager.scanner.resultsFileName, scanner.workingDirs[0], utils.Sast) + + // Assert + assert.NoError(t, err) + assert.NotEmpty(t, sastScanManager.sastScannerResults) + // File has 4 results, 2 of them at the same location different codeFlow + assert.Equal(t, 3, len(sastScanManager.sastScannerResults)) +} diff --git a/xray/audit/jas/secretsscanner.go b/xray/audit/jas/secretsscanner.go index cd1159a49..38648e47b 100644 --- a/xray/audit/jas/secretsscanner.go +++ b/xray/audit/jas/secretsscanner.go @@ -1,6 +1,7 @@ package jas import ( + "path/filepath" "strings" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" @@ -13,7 +14,7 @@ const ( ) type SecretScanManager struct { - secretsScannerResults []utils.IacOrSecretResult + secretsScannerResults []utils.SourceCodeScanResult scanner *AdvancedSecurityScanner } @@ -24,7 +25,7 @@ type SecretScanManager struct { // Return values: // []utils.IacOrSecretResult: a list of the secrets that were found. // error: An error object (if any). -func getSecretsScanResults(scanner *AdvancedSecurityScanner) (results []utils.IacOrSecretResult, err error) { +func getSecretsScanResults(scanner *AdvancedSecurityScanner) (results []utils.SourceCodeScanResult, err error) { secretScanManager := newSecretsScanManager(scanner) log.Info("Running secrets scanning...") if err = secretScanManager.scanner.Run(secretScanManager); err != nil { @@ -33,14 +34,14 @@ func getSecretsScanResults(scanner *AdvancedSecurityScanner) (results []utils.Ia } results = secretScanManager.secretsScannerResults if len(results) > 0 { - log.Info(len(results), "secrets were found") + log.Info("Found", len(results), "secrets") } return } func newSecretsScanManager(scanner *AdvancedSecurityScanner) (manager *SecretScanManager) { return &SecretScanManager{ - secretsScannerResults: []utils.IacOrSecretResult{}, + secretsScannerResults: []utils.SourceCodeScanResult{}, scanner: scanner, } } @@ -53,8 +54,10 @@ func (s *SecretScanManager) Run(wd string) (err error) { if err = s.runAnalyzerManager(); err != nil { return } - var workingDirResults []utils.IacOrSecretResult - workingDirResults, err = getIacOrSecretsScanResults(scanner.resultsFileName, wd, true) + var workingDirResults []utils.SourceCodeScanResult + if workingDirResults, err = getSourceCodeScanResults(scanner.resultsFileName, wd, utils.Secrets); err != nil { + return + } s.secretsScannerResults = append(s.secretsScannerResults, workingDirResults...) return } @@ -85,7 +88,7 @@ func (s *SecretScanManager) createConfigFile(currentWd string) error { } func (s *SecretScanManager) runAnalyzerManager() error { - return s.scanner.analyzerManager.Exec(s.scanner.configFileName, secretsScanCommand, s.scanner.serverDetails) + return s.scanner.analyzerManager.Exec(s.scanner.configFileName, secretsScanCommand, filepath.Dir(s.scanner.analyzerManager.AnalyzerManagerFullPath), s.scanner.serverDetails) } func hideSecret(secret string) string { diff --git a/xray/audit/jas/secretsscanner_test.go b/xray/audit/jas/secretsscanner_test.go index 9ec80fa61..b309532ee 100644 --- a/xray/audit/jas/secretsscanner_test.go +++ b/xray/audit/jas/secretsscanner_test.go @@ -1,26 +1,20 @@ package jas import ( - rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/stretchr/testify/assert" "os" "path/filepath" "testing" + + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/stretchr/testify/assert" ) func TestNewSecretsScanManager(t *testing.T) { - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t) + defer cleanUp() secretScanManager := newSecretsScanManager(scanner) - assert.NoError(t, err) assert.NotEmpty(t, secretScanManager) assert.NotEmpty(t, secretScanManager.scanner.configFileName) assert.NotEmpty(t, secretScanManager.scanner.resultsFileName) @@ -28,14 +22,8 @@ func TestNewSecretsScanManager(t *testing.T) { } func TestSecretsScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t) + defer cleanUp() secretScanManager := newSecretsScanManager(scanner) currWd, err := coreutils.GetWorkingDirectory() @@ -60,39 +48,23 @@ func TestRunAnalyzerManager_ReturnsGeneralError(t *testing.T) { os.Clearenv() }() - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - secretScanManager := newSecretsScanManager(scanner) + scanner, cleanUp := initJasTest(t) + defer cleanUp() - // Act - err = secretScanManager.runAnalyzerManager() - - // Assert - assert.Error(t, err) + secretScanManager := newSecretsScanManager(scanner) + assert.Error(t, secretScanManager.runAnalyzerManager()) } func TestParseResults_EmptyResults(t *testing.T) { + scanner, cleanUp := initJasTest(t) + defer cleanUp() // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() secretScanManager := newSecretsScanManager(scanner) secretScanManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "secrets-scan", "no-secrets.sarif") // Act - secretScanManager.secretsScannerResults, err = getIacOrSecretsScanResults(secretScanManager.scanner.resultsFileName, scanner.workingDirs[0], false) + var err error + secretScanManager.secretsScannerResults, err = getSourceCodeScanResults(secretScanManager.scanner.resultsFileName, scanner.workingDirs[0], utils.Secrets) // Assert assert.NoError(t, err) @@ -101,19 +73,15 @@ func TestParseResults_EmptyResults(t *testing.T) { func TestParseResults_ResultsContainSecrets(t *testing.T) { // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t) + defer cleanUp() + secretScanManager := newSecretsScanManager(scanner) secretScanManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "secrets-scan", "contain-secrets.sarif") // Act - secretScanManager.secretsScannerResults, err = getIacOrSecretsScanResults(secretScanManager.scanner.resultsFileName, scanner.workingDirs[0], false) + var err error + secretScanManager.secretsScannerResults, err = getSourceCodeScanResults(secretScanManager.scanner.resultsFileName, scanner.workingDirs[0], utils.Secrets) // Assert assert.NoError(t, err) @@ -122,14 +90,9 @@ func TestParseResults_ResultsContainSecrets(t *testing.T) { } func TestGetSecretsScanResults_AnalyzerManagerReturnsError(t *testing.T) { - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() + scanner, cleanUp := initJasTest(t) + defer cleanUp() + secretsResults, err := getSecretsScanResults(scanner) assert.Error(t, err) diff --git a/xray/commands/audit/generic/generic.go b/xray/commands/audit/generic/generic.go index 4e182632b..939d78db4 100644 --- a/xray/commands/audit/generic/generic.go +++ b/xray/commands/audit/generic/generic.go @@ -97,7 +97,7 @@ func (auditCmd *GenericAuditCommand) Run() (err error) { } var messages []string if !auditResults.ExtendedScanResults.EntitledForJas { - messages = []string{coreutils.PrintTitle("The ‘jf audit’ command also supports JFrog Advanced Security features, such as 'Contextual Analysis', 'Secret Detection', 'IaC Scan'.\nThis feature isn't enabled on your system. Read more - ") + coreutils.PrintLink("https://jfrog.com/xray/")} + messages = []string{coreutils.PrintTitle("The ‘jf audit’ command also supports JFrog Advanced Security features, such as 'Contextual Analysis', 'Secret Detection', 'IaC Scan' and ‘SAST’.\nThis feature isn't enabled on your system. Read more - ") + coreutils.PrintLink("https://jfrog.com/xray/")} } // Print Scan results on all cases except if errors accrued on SCA scan and no security/license issues found. printScanResults := !(auditResults.ScaError != nil && xrutils.IsEmptyScanResponse(auditResults.ExtendedScanResults.XrayResults)) diff --git a/xray/commands/testdata/sast-scan/contains-sast-violations.sarif b/xray/commands/testdata/sast-scan/contains-sast-violations.sarif new file mode 100644 index 000000000..d8b3c02e4 --- /dev/null +++ b/xray/commands/testdata/sast-scan/contains-sast-violations.sarif @@ -0,0 +1,907 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "USAF", + "rules": [ + { + "id": "python-command-injection", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "78" + } + } + }, + "fullDescription": { + "text": "\nRemote Code Execution is a type of vulnerability that allows an attacker\nto execute arbitrary code on a remote computer or device.\nThis can allow the attacker to gain full control of the target system, potentially\nleading to sensitive information being compromised or unauthorized actions being performed.\n\nIn this query we look for user inputs that can flow directly into execution commands\nin Python. There are many types of Command Injection, so in the future we will make\nfine-tuning changes that will need to apply to this query in the future.\n", + "markdown": "\nRemote Code Execution is a type of vulnerability that allows an attacker\nto execute arbitrary code on a remote computer or device.\nThis can allow the attacker to gain full control of the target system, potentially\nleading to sensitive information being compromised or unauthorized actions being performed.\n\nIn this query we look for user inputs that can flow directly into execution commands\nin Python. There are many types of Command Injection, so in the future we will make\nfine-tuning changes that will need to apply to this query in the future.\n" + }, + "shortDescription": { + "text": "Command Injection" + } + }, + { + "id": "python-flask-debug", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "1295" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nDebug mode in a Flask app is a feature that allows the developer to see detailed\nerror messages and tracebacks when an error occurs. This can be useful for debugging\nand troubleshooting, but it can also create a security vulnerability if the app is\ndeployed in debug mode. In debug mode, Flask will display detailed error messages and\ntracebacks to the user, even if the error is caused by malicious input.\nThis can provide attackers with valuable information about the app's internal workings\nand vulnerabilities, making it easier for them to exploit those vulnerabilities.\n\n### Query operation\nIn this query we look Flask applications that set the `debug` argument to `True`\n\n### Vulnerable example\n```python\nfrom flask import Flask\n\napp = Flask(__name__)\n\n@app.route('/')\ndef hello():\n return 'Hello, World!'\n\nif __name__ == '__main__':\n app.run(debug=True)\n```\nIn this example, the Flask application is set to run in debug mode by passing\n`debug=True` as an argument to the `app.run()` function. This will make the application\nemit potentially sensitive information to the users.\n\n### Remediation\nWhen using `app.run`, omit the `debug` flag or set it to `False` -\n```diff\nif __name__ == '__main__':\n- app.run(debug=True)\n+ app.run()\n```\n", + "markdown": "\n### Overview\nDebug mode in a Flask app is a feature that allows the developer to see detailed\nerror messages and tracebacks when an error occurs. This can be useful for debugging\nand troubleshooting, but it can also create a security vulnerability if the app is\ndeployed in debug mode. In debug mode, Flask will display detailed error messages and\ntracebacks to the user, even if the error is caused by malicious input.\nThis can provide attackers with valuable information about the app's internal workings\nand vulnerabilities, making it easier for them to exploit those vulnerabilities.\n\n### Query operation\nIn this query we look Flask applications that set the `debug` argument to `True`\n\n### Vulnerable example\n```python\nfrom flask import Flask\n\napp = Flask(__name__)\n\n@app.route('/')\ndef hello():\n return 'Hello, World!'\n\nif __name__ == '__main__':\n app.run(debug=True)\n```\nIn this example, the Flask application is set to run in debug mode by passing\n`debug=True` as an argument to the `app.run()` function. This will make the application\nemit potentially sensitive information to the users.\n\n### Remediation\nWhen using `app.run`, omit the `debug` flag or set it to `False` -\n```diff\nif __name__ == '__main__':\n- app.run(debug=True)\n+ app.run()\n```\n" + }, + "shortDescription": { + "text": "Flask Running in Debug" + } + }, + { + "id": "python-open-redirect", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "601" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nAn open redirect is a type of vulnerability that occurs when a web application or website\nredirects a user to an arbitrary URL, without properly validating the destination URL.\nThis can allow an attacker to redirect a user to a malicious website via a trusted website,\npotentially tricking the user into providing sensitive information or downloading malware.\n\n### Query operation\nIn this query we look for redirections that are affected by any user input.\n\n### Vulnerable example\nIn the following example, the application has a route `/redirect`\nthat takes a query parameter `url` and performs a redirection to that URL\nusing Flask's redirect() function.\n```python\nfrom flask import Flask, request, redirect\n\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n return \"\"\"\n

Welcome to Example App

\n Click here to visit Google.\n \"\"\"\n\n@app.route('/redirect')\ndef redirect_to_external():\n url = request.args.get('url', '/')\n return redirect(url)\n\nif __name__ == '__main__':\n app.run()\n```\nThe vulnerability lies in the fact that the application does not validate or sanitize the\n`url` parameter, allowing an attacker to redirect users to malicious or unintended websites.\nAn attacker could exploit this vulnerability by modifying the `url` parameter to a different\nsite, such as:\n`http://localhost:5000/redirect?url=https://www.malicious.com`\n\n### Remediation\nBefore redirection, check whether the target URL leads to a trusted domain, for example by\nusing a whitelist -\n```python\ndef is_safe_url(url):\n # Whitelist trusted domains\n trusted_domains = ['https://www.google.com', 'https://www.example.com']\n\n # Check if the provided URL is in the trusted domains\n for domain in trusted_domains:\n if url.startswith(domain):\n return True\n\n return False\n```\n\n```diff\n@app.route('/redirect')\ndef redirect_to_external():\n url = request.args.get('url', '/')\n\n # Validate the URL to ensure it's a trusted destination\n+ if is_safe_url(url):\n+ return redirect(url)\n+ else:\n+ abort(400) # Bad Request\n```\n", + "markdown": "\n### Overview\nAn open redirect is a type of vulnerability that occurs when a web application or website\nredirects a user to an arbitrary URL, without properly validating the destination URL.\nThis can allow an attacker to redirect a user to a malicious website via a trusted website,\npotentially tricking the user into providing sensitive information or downloading malware.\n\n### Query operation\nIn this query we look for redirections that are affected by any user input.\n\n### Vulnerable example\nIn the following example, the application has a route `/redirect`\nthat takes a query parameter `url` and performs a redirection to that URL\nusing Flask's redirect() function.\n```python\nfrom flask import Flask, request, redirect\n\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n return \"\"\"\n

Welcome to Example App

\n Click here to visit Google.\n \"\"\"\n\n@app.route('/redirect')\ndef redirect_to_external():\n url = request.args.get('url', '/')\n return redirect(url)\n\nif __name__ == '__main__':\n app.run()\n```\nThe vulnerability lies in the fact that the application does not validate or sanitize the\n`url` parameter, allowing an attacker to redirect users to malicious or unintended websites.\nAn attacker could exploit this vulnerability by modifying the `url` parameter to a different\nsite, such as:\n`http://localhost:5000/redirect?url=https://www.malicious.com`\n\n### Remediation\nBefore redirection, check whether the target URL leads to a trusted domain, for example by\nusing a whitelist -\n```python\ndef is_safe_url(url):\n # Whitelist trusted domains\n trusted_domains = ['https://www.google.com', 'https://www.example.com']\n\n # Check if the provided URL is in the trusted domains\n for domain in trusted_domains:\n if url.startswith(domain):\n return True\n\n return False\n```\n\n```diff\n@app.route('/redirect')\ndef redirect_to_external():\n url = request.args.get('url', '/')\n\n # Validate the URL to ensure it's a trusted destination\n+ if is_safe_url(url):\n+ return redirect(url)\n+ else:\n+ abort(400) # Bad Request\n```\n" + }, + "shortDescription": { + "text": "Open Redirect" + } + }, + { + "id": "python-parameter-injection", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "74" + } + } + }, + "fullDescription": { + "text": "\nRemote Code Execution is a type of vulnerability that allows an attacker\nto execute arbitrary code on a remote computer or device.\nThis can allow the attacker to gain full control of the target system, potentially\nleading to sensitive information being compromised or unauthorized actions being performed.\n\nIn this query we look for user inputs that can flow directly into execution commands\nin Python. There are many types of Command Injection, so in the future we will make\nfine-tuning changes that will need to apply to this query in the future.\n", + "markdown": "\nRemote Code Execution is a type of vulnerability that allows an attacker\nto execute arbitrary code on a remote computer or device.\nThis can allow the attacker to gain full control of the target system, potentially\nleading to sensitive information being compromised or unauthorized actions being performed.\n\nIn this query we look for user inputs that can flow directly into execution commands\nin Python. There are many types of Command Injection, so in the future we will make\nfine-tuning changes that will need to apply to this query in the future.\n" + }, + "shortDescription": { + "text": "Parameter Injection" + } + }, + { + "id": "python-path-traversal", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "22" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nPath traversal, also known as directory traversal, is a type of vulnerability that allows an\nattacker to access files or directories on a computer or device that are outside of\nthe intended directory.\nAllowing arbitrary read access can allow the attacker to read sensitive files, such as\nconfiguration files or sensitive data, potentially leading data loss\nor even system compromise.\nAllowing arbitrary write access is more severe and in most cases leads to arbitrary code\nexecution, via editing important system files or sensitive data.\n\n### Query operation\nIn this query we look for user input that can flow un-sanitized as a path into file access\nfunctions\n(either read or write access)\n\n### Vulnerable example\n```python\nfrom flask import Flask, request, send_file\napp = Flask(__name__)\n\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n return send_file(basepath + filename)\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application has a route `/files/` that serves files from a directory\ncalled `static/files`. The vulnerability lies in the fact that the application does not\nproperly validate or sanitize the `filename` parameter, allowing an attacker to traverse\nbeyond the intended directory and access sensitive files on the server.\nAn attacker could exploit this vulnerability by manipulating the `filename` parameter\nand providing a relative path to access files outside of the `static/files` directory.\nFor example, they could craft a URL like this:\n`http://localhost:5000/files/?filename=../../../etc/passwd`\n\n### Remediation\nWhen possible, use inherently safe path functions such as `send_from_directory` that perform\nfilename escaping -\n```diff\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n- return send_file(basepath + filename)\n+ return send_from_directory(basepath, filename)\n```\nAlternatively, before accessing a potential path, check that the user's `filename` does not\nescape the intended path -\n```python\nfrom pathlib import Path\ndef is_escaping_path(basepath, userpath):\n try:\n Path(basepath).joinpath(userpath).resolve().relative_to(basepath.resolve())\n return False\n except ValueError:\n return True\n```\n\n```diff\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n+ if is_escaping_path(basepath, filename):\n+ abort(400) # Bad Request\n return send_file(basepath + filename)\n```\nAlternatively - use inherently safe\n", + "markdown": "\n### Overview\nPath traversal, also known as directory traversal, is a type of vulnerability that allows an\nattacker to access files or directories on a computer or device that are outside of\nthe intended directory.\nAllowing arbitrary read access can allow the attacker to read sensitive files, such as\nconfiguration files or sensitive data, potentially leading data loss\nor even system compromise.\nAllowing arbitrary write access is more severe and in most cases leads to arbitrary code\nexecution, via editing important system files or sensitive data.\n\n### Query operation\nIn this query we look for user input that can flow un-sanitized as a path into file access\nfunctions\n(either read or write access)\n\n### Vulnerable example\n```python\nfrom flask import Flask, request, send_file\napp = Flask(__name__)\n\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n return send_file(basepath + filename)\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application has a route `/files/` that serves files from a directory\ncalled `static/files`. The vulnerability lies in the fact that the application does not\nproperly validate or sanitize the `filename` parameter, allowing an attacker to traverse\nbeyond the intended directory and access sensitive files on the server.\nAn attacker could exploit this vulnerability by manipulating the `filename` parameter\nand providing a relative path to access files outside of the `static/files` directory.\nFor example, they could craft a URL like this:\n`http://localhost:5000/files/?filename=../../../etc/passwd`\n\n### Remediation\nWhen possible, use inherently safe path functions such as `send_from_directory` that perform\nfilename escaping -\n```diff\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n- return send_file(basepath + filename)\n+ return send_from_directory(basepath, filename)\n```\nAlternatively, before accessing a potential path, check that the user's `filename` does not\nescape the intended path -\n```python\nfrom pathlib import Path\ndef is_escaping_path(basepath, userpath):\n try:\n Path(basepath).joinpath(userpath).resolve().relative_to(basepath.resolve())\n return False\n except ValueError:\n return True\n```\n\n```diff\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n+ if is_escaping_path(basepath, filename):\n+ abort(400) # Bad Request\n return send_file(basepath + filename)\n```\nAlternatively - use inherently safe\n" + }, + "shortDescription": { + "text": "Path Traversal" + } + }, + { + "id": "python-sqli", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "89" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nSQL injection is a type of vulnerability that allows an attacker to execute arbitrary SQL\ncommands on a database.\nThis can allow the attacker to gain access to sensitive information,\nsuch as user credentials or sensitive data, or to perform unauthorized actions,\nsuch as deleting or modifying data.\n\n### Query operation\nIn this query we check if a user input can flow un-sanitized into an SQL query.\n\n### Vulnerable example\n```python\nfrom flask import Flask, request\nimport sqlite3\n\napp = Flask(__name__)\n\n@app.route('/login', methods=['POST'])\ndef login():\n username = request.form.get('username')\n password = request.form.get('password')\n\n # Vulnerable SQL query\n query = f\"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'\"\n\n # Execute the query\n conn = sqlite3.connect('database.db')\n cursor = conn.cursor()\n cursor.execute(query)\n user = cursor.fetchone()\n conn.close()\n\n if user:\n return 'Login successful'\n else:\n return 'Login failed'\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application accepts a `username` and `password` from a login form via a\nPOST request. The SQL query is constructed using string concatenation, which makes it\nvulnerable to SQL injection attacks.\n\nAn attacker can exploit this vulnerability by entering `' OR 1=1 --` as the `username`.\nThe resulting query would become -\n```sql\nSELECT * FROM users WHERE username = '' OR 1=1 --' AND password = ''\n```\nwhich will always evaluate to TRUE, leading to an authentication bypass\nsince the attacker has no valid credentials.\n\n### Remediation\nReplace the vulnerable string concatenation with a parameterized query\nusing `?` placeholders -\n```diff\n@app.route('/login', methods=['POST'])\n def login():\n username = request.form.get('username')\n password = request.form.get('password')\n\n # Vulnerable SQL query\n- query = f\"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'\"\n+ query = \"SELECT * FROM users WHERE username = ? AND password = ?\"\n\n # Execute the query\n conn = sqlite3.connect('database.db')\n cursor = conn.cursor()\n- cursor.execute(query)\n+ cursor.execute(query, (username, password))\n user = cursor.fetchone()\n conn.close()\n\n if user:\n return 'Login successful'\n else:\n return 'Login failed'\n```\n", + "markdown": "\n### Overview\nSQL injection is a type of vulnerability that allows an attacker to execute arbitrary SQL\ncommands on a database.\nThis can allow the attacker to gain access to sensitive information,\nsuch as user credentials or sensitive data, or to perform unauthorized actions,\nsuch as deleting or modifying data.\n\n### Query operation\nIn this query we check if a user input can flow un-sanitized into an SQL query.\n\n### Vulnerable example\n```python\nfrom flask import Flask, request\nimport sqlite3\n\napp = Flask(__name__)\n\n@app.route('/login', methods=['POST'])\ndef login():\n username = request.form.get('username')\n password = request.form.get('password')\n\n # Vulnerable SQL query\n query = f\"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'\"\n\n # Execute the query\n conn = sqlite3.connect('database.db')\n cursor = conn.cursor()\n cursor.execute(query)\n user = cursor.fetchone()\n conn.close()\n\n if user:\n return 'Login successful'\n else:\n return 'Login failed'\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application accepts a `username` and `password` from a login form via a\nPOST request. The SQL query is constructed using string concatenation, which makes it\nvulnerable to SQL injection attacks.\n\nAn attacker can exploit this vulnerability by entering `' OR 1=1 --` as the `username`.\nThe resulting query would become -\n```sql\nSELECT * FROM users WHERE username = '' OR 1=1 --' AND password = ''\n```\nwhich will always evaluate to TRUE, leading to an authentication bypass\nsince the attacker has no valid credentials.\n\n### Remediation\nReplace the vulnerable string concatenation with a parameterized query\nusing `?` placeholders -\n```diff\n@app.route('/login', methods=['POST'])\n def login():\n username = request.form.get('username')\n password = request.form.get('password')\n\n # Vulnerable SQL query\n- query = f\"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'\"\n+ query = \"SELECT * FROM users WHERE username = ? AND password = ?\"\n\n # Execute the query\n conn = sqlite3.connect('database.db')\n cursor = conn.cursor()\n- cursor.execute(query)\n+ cursor.execute(query, (username, password))\n user = cursor.fetchone()\n conn.close()\n\n if user:\n return 'Login successful'\n else:\n return 'Login failed'\n```\n" + }, + "shortDescription": { + "text": "SQL Injection" + } + }, + { + "id": "python-stack-trace-exposure", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "209" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nStack trace exposure is a type of security vulnerability that occurs when a program reveals\nsensitive information, such as the names and locations of internal files and variables,\nin error messages or other diagnostic output. This can happen when a program crashes or\nencounters an error, and the stack trace (a record of the program's call stack at the time\nof the error) is included in the output. Stack trace exposure can provide attackers with\nvaluable information about a program's internal workings and vulnerabilities, making it\neasier for them to exploit those vulnerabilities and gain unauthorized access\nto the system.\n\n### Query operation\nIn this query we look for any stack trace information flowing into the output.\n\n### Vulnerable example\n```python\nimport traceback\n\ndef my_function():\n try:\n # Some code that may raise an exception\n raise ValueError('Something went wrong')\n except ValueError as e:\n traceback.print_tb(e.__traceback__)\n\nmy_function()\n```\nIn this example, the `my_function()` function intentionally raises\na `ValueError` exception.\nThe `traceback.print_tb()` function is then used to print the stack trace\nwhen the exception is caught. The vulnerability lies in using `traceback.print_tb()`\nto output the stack trace directly to the console or any other output stream.\nIf this code were part of a web application or exposed through an API,\nthe stack trace would be exposed in the server logs or potentially returned\nas part of an error response to the client.\n\n### Remediation\nLog the exception to a logging framework or file, instead of outputting directly to the\nconsole-\n\n```python\ndef log_exception(exception):\n logging.exception('An exception occurred', exc_info=exception)\n```\n\n```diff\ndef my_function():\n try:\n # Some code that may raise an exception\n raise ValueError('Something went wrong')\n except ValueError as e:\n- traceback.print_tb(e.__traceback__)\n+ log_exception(e)\n```\n", + "markdown": "\n### Overview\nStack trace exposure is a type of security vulnerability that occurs when a program reveals\nsensitive information, such as the names and locations of internal files and variables,\nin error messages or other diagnostic output. This can happen when a program crashes or\nencounters an error, and the stack trace (a record of the program's call stack at the time\nof the error) is included in the output. Stack trace exposure can provide attackers with\nvaluable information about a program's internal workings and vulnerabilities, making it\neasier for them to exploit those vulnerabilities and gain unauthorized access\nto the system.\n\n### Query operation\nIn this query we look for any stack trace information flowing into the output.\n\n### Vulnerable example\n```python\nimport traceback\n\ndef my_function():\n try:\n # Some code that may raise an exception\n raise ValueError('Something went wrong')\n except ValueError as e:\n traceback.print_tb(e.__traceback__)\n\nmy_function()\n```\nIn this example, the `my_function()` function intentionally raises\na `ValueError` exception.\nThe `traceback.print_tb()` function is then used to print the stack trace\nwhen the exception is caught. The vulnerability lies in using `traceback.print_tb()`\nto output the stack trace directly to the console or any other output stream.\nIf this code were part of a web application or exposed through an API,\nthe stack trace would be exposed in the server logs or potentially returned\nas part of an error response to the client.\n\n### Remediation\nLog the exception to a logging framework or file, instead of outputting directly to the\nconsole-\n\n```python\ndef log_exception(exception):\n logging.exception('An exception occurred', exc_info=exception)\n```\n\n```diff\ndef my_function():\n try:\n # Some code that may raise an exception\n raise ValueError('Something went wrong')\n except ValueError as e:\n- traceback.print_tb(e.__traceback__)\n+ log_exception(e)\n```\n" + }, + "shortDescription": { + "text": "Stack Trace Exposure" + } + }, + { + "id": "python-unsafe-deserialization", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "502" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nUnsafe deserialization is a security vulnerability that occurs when a program deserializes\nuntrusted data with a potentially dangerous deserializer.\nDeserialization is the process of converting serialized data (data that\nhas been converted into a format that can be easily transmitted or stored) back into its\noriginal form. In some (\"unsafe\") serialization protocols, if an attacker is able to\nmanipulate the serialized data, they may be able to execute arbitrary code or perform other\nmalicious actions when the data is deserialized.\n\n### Query operation\nIn this query we look for user input that can flow un-sanitized to potentially unsafe\ndeserialization methods\n\n### Vulnerable example\n```python\nimport yaml\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n@app.route('/process', methods=['POST'])\ndef process():\n data = request.get_data()\n\n # Vulnerable deserialization\n obj = yaml.load(data)\n\n # Process the deserialized object (for simplicity, we're just printing it)\n print(obj)\n\n return 'Data processed'\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application exposes a `/process` endpoint that accepts data via a POST\nrequest. The vulnerable code uses the `yaml.load()` function\nto deserialize the received data.\nThe vulnerability lies in the fact that the `yaml` module can execute arbitrary code\nduring the deserialization process.\nAn attacker can exploit this by crafting a malicious payload\nthat executes arbitrary code when the `yaml.load()` function is called.\n\n### Remediation\nUse deserialization routines that are known to handle untrusted data securely, such as\n`yaml.safe_load`. It is highly recommended to use the `json` module for serialization, as it\ndeserializes untrusted data securely.\n\n```diff\n@app.route('/process', methods=['POST'])\ndef process():\n data = request.get_data()\n\n # Safe deserialization\n- obj = yaml.load(data)\n+ obj = yaml.safe_load(data)\n\n # Process the deserialized object (for simplicity, we're just printing it)\n print(obj)\n\n return 'Data processed'\n```\n", + "markdown": "\n### Overview\nUnsafe deserialization is a security vulnerability that occurs when a program deserializes\nuntrusted data with a potentially dangerous deserializer.\nDeserialization is the process of converting serialized data (data that\nhas been converted into a format that can be easily transmitted or stored) back into its\noriginal form. In some (\"unsafe\") serialization protocols, if an attacker is able to\nmanipulate the serialized data, they may be able to execute arbitrary code or perform other\nmalicious actions when the data is deserialized.\n\n### Query operation\nIn this query we look for user input that can flow un-sanitized to potentially unsafe\ndeserialization methods\n\n### Vulnerable example\n```python\nimport yaml\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n@app.route('/process', methods=['POST'])\ndef process():\n data = request.get_data()\n\n # Vulnerable deserialization\n obj = yaml.load(data)\n\n # Process the deserialized object (for simplicity, we're just printing it)\n print(obj)\n\n return 'Data processed'\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application exposes a `/process` endpoint that accepts data via a POST\nrequest. The vulnerable code uses the `yaml.load()` function\nto deserialize the received data.\nThe vulnerability lies in the fact that the `yaml` module can execute arbitrary code\nduring the deserialization process.\nAn attacker can exploit this by crafting a malicious payload\nthat executes arbitrary code when the `yaml.load()` function is called.\n\n### Remediation\nUse deserialization routines that are known to handle untrusted data securely, such as\n`yaml.safe_load`. It is highly recommended to use the `json` module for serialization, as it\ndeserializes untrusted data securely.\n\n```diff\n@app.route('/process', methods=['POST'])\ndef process():\n data = request.get_data()\n\n # Safe deserialization\n- obj = yaml.load(data)\n+ obj = yaml.safe_load(data)\n\n # Process the deserialized object (for simplicity, we're just printing it)\n print(obj)\n\n return 'Data processed'\n```\n" + }, + "shortDescription": { + "text": "Unsafe Deserialization" + } + }, + { + "id": "python-xss", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "79" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nXSS, or Cross-Site Scripting, is a type of vulnerability that allows an attacker to\ninject malicious code into a website or web application.\nThis can allow the attacker to steal sensitive information from users, such as their\ncookies or login credentials, or to perform unauthorized actions on their behalf.\n\n### Query operation\nIn the query we look for any user input that flows into\na potential output of the application.\n\n### Vulnerable example\nIn the following example, the Flask application takes a user-supplied parameter (`name`)\nfrom the query string and renders it directly into an HTML template using the\n`render_template_string` function. The issue is that\nthe user input is not properly sanitized or escaped, making it vulnerable to XSS attacks.\n```python\nfrom flask import Flask, request, render_template_string\n\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n name = request.args.get('name', 'Guest')\n message = f'Hello, {name}!'\n return render_template_string('

{}

'.format(message))\n\nif __name__ == '__main__':\napp.run()\n```\nAn attacker can exploit this vulnerability by injecting malicious JavaScript code into the\n`name` parameter. For instance, they could modify the URL to include the following payload:\n`http://localhost:5000/?name=`\n\n### Remediation\nWhen rendering templates, use parametrized variable assignments (which are automatically\nescaped) instead of direct string manipulation -\n```diff\n@app.route('/')\ndef index():\n name = request.args.get('name', 'Guest')\n message = f'Hello, {name}!'\n- return render_template_string('

{}

'.format(message))\n+ return render_template_string('

{{ message }}

', message=message)\n```\n", + "markdown": "\n### Overview\nXSS, or Cross-Site Scripting, is a type of vulnerability that allows an attacker to\ninject malicious code into a website or web application.\nThis can allow the attacker to steal sensitive information from users, such as their\ncookies or login credentials, or to perform unauthorized actions on their behalf.\n\n### Query operation\nIn the query we look for any user input that flows into\na potential output of the application.\n\n### Vulnerable example\nIn the following example, the Flask application takes a user-supplied parameter (`name`)\nfrom the query string and renders it directly into an HTML template using the\n`render_template_string` function. The issue is that\nthe user input is not properly sanitized or escaped, making it vulnerable to XSS attacks.\n```python\nfrom flask import Flask, request, render_template_string\n\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n name = request.args.get('name', 'Guest')\n message = f'Hello, {name}!'\n return render_template_string('

{}

'.format(message))\n\nif __name__ == '__main__':\napp.run()\n```\nAn attacker can exploit this vulnerability by injecting malicious JavaScript code into the\n`name` parameter. For instance, they could modify the URL to include the following payload:\n`http://localhost:5000/?name=`\n\n### Remediation\nWhen rendering templates, use parametrized variable assignments (which are automatically\nescaped) instead of direct string manipulation -\n```diff\n@app.route('/')\ndef index():\n name = request.args.get('name', 'Guest')\n message = f'Hello, {name}!'\n- return render_template_string('

{}

'.format(message))\n+ return render_template_string('

{{ message }}

', message=message)\n```\n" + }, + "shortDescription": { + "text": "XSS Vulnerability" + } + } + ] + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "/Users/assafa/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", + "scan", + "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1693477603-3697552683/results.sarif" + ], + "workingDirectory": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat" + } + } + ], + "results": [ + { + "message": { + "text": "SQL Injection" + }, + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 28, + "endLine": 9, + "snippet": { + "text": "request.form" + }, + "startColumn": 16, + "startLine": 9 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 32, + "endLine": 9, + "snippet": { + "text": "request.form.get" + }, + "startColumn": 16, + "startLine": 9 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 44, + "endLine": 9, + "snippet": { + "text": "request.form.get(\"username\")" + }, + "startColumn": 16, + "startLine": 9 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 13, + "endLine": 9, + "snippet": { + "text": "username" + }, + "startColumn": 5, + "startLine": 9 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 31, + "endLine": 20, + "snippet": { + "text": "(username, password)" + }, + "startColumn": 11, + "startLine": 20 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 31, + "endLine": 20, + "snippet": { + "text": "\"SELECT id, username, access_level FROM user WHERE username = '%s' AND password = '%s'\"\n % (username, password)" + }, + "startColumn": 9, + "startLine": 19 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 10, + "endLine": 18, + "snippet": { + "text": "query" + }, + "startColumn": 5, + "startLine": 18 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 39, + "endLine": 22, + "snippet": { + "text": "query_db(query, [], True)" + }, + "startColumn": 14, + "startLine": 22 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 19, + "endLine": 10, + "snippet": { + "text": "query" + }, + "startColumn": 14, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 49, + "endLine": 14, + "snippet": { + "text": "conn.cursor().execute(query, args)" + }, + "startColumn": 15, + "startLine": 14 + } + } + } + } + ] + } + ] + } + ], + "level": "error", + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 49, + "endLine": 14, + "snippet": { + "text": "conn.cursor().execute(query, args)" + }, + "startColumn": 15, + "startLine": 14 + } + } + } + ], + "ruleId": "python-sqli" + }, + { + "message": { + "text": "SQL Injection" + }, + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 28, + "endLine": 10, + "snippet": { + "text": "request.form" + }, + "startColumn": 16, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 32, + "endLine": 10, + "snippet": { + "text": "request.form.get" + }, + "startColumn": 16, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 44, + "endLine": 10, + "snippet": { + "text": "request.form.get(\"password\")" + }, + "startColumn": 16, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 13, + "endLine": 10, + "snippet": { + "text": "password" + }, + "startColumn": 5, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 31, + "endLine": 20, + "snippet": { + "text": "(username, password)" + }, + "startColumn": 11, + "startLine": 20 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 31, + "endLine": 20, + "snippet": { + "text": "\"SELECT id, username, access_level FROM user WHERE username = '%s' AND password = '%s'\"\n % (username, password)" + }, + "startColumn": 9, + "startLine": 19 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 10, + "endLine": 18, + "snippet": { + "text": "query" + }, + "startColumn": 5, + "startLine": 18 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 39, + "endLine": 22, + "snippet": { + "text": "query_db(query, [], True)" + }, + "startColumn": 14, + "startLine": 22 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 19, + "endLine": 10, + "snippet": { + "text": "query" + }, + "startColumn": 14, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 49, + "endLine": 14, + "snippet": { + "text": "conn.cursor().execute(query, args)" + }, + "startColumn": 15, + "startLine": 14 + } + } + } + } + ] + } + ] + } + ], + "level": "error", + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 49, + "endLine": 14, + "snippet": { + "text": "conn.cursor().execute(query, args)" + }, + "startColumn": 15, + "startLine": 14 + } + } + } + ], + "ruleId": "python-sqli" + }, + { + "message": { + "text": "Open Redirect" + }, + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 23, + "endLine": 33, + "snippet": { + "text": "request.args" + }, + "startColumn": 11, + "startLine": 33 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 27, + "endLine": 33, + "snippet": { + "text": "request.args.get" + }, + "startColumn": 11, + "startLine": 33 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 34, + "endLine": 33, + "snippet": { + "text": "request.args.get(\"url\")" + }, + "startColumn": 11, + "startLine": 33 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 8, + "endLine": 33, + "snippet": { + "text": "url" + }, + "startColumn": 5, + "startLine": 33 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 29, + "endLine": 46, + "snippet": { + "text": "redirect(url)" + }, + "startColumn": 16, + "startLine": 46 + } + } + } + } + ] + } + ] + } + ], + "level": "note", + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 29, + "endLine": 46, + "snippet": { + "text": "redirect(url)" + }, + "startColumn": 16, + "startLine": 46 + } + } + } + ], + "ruleId": "python-open-redirect" + }, + { + "message": { + "text": "Flask Running in Debug" + }, + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "run" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/run.py" + }, + "region": { + "endColumn": 24, + "endLine": 15, + "snippet": { + "text": "app.run(debug=True)" + }, + "startColumn": 5, + "startLine": 15 + } + } + } + ], + "ruleId": "python-flask-debug" + } + ] + } + ], + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" +} \ No newline at end of file diff --git a/xray/commands/testdata/sast-scan/no-violations.sarif b/xray/commands/testdata/sast-scan/no-violations.sarif new file mode 100644 index 000000000..ed129e6e0 --- /dev/null +++ b/xray/commands/testdata/sast-scan/no-violations.sarif @@ -0,0 +1,28 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "USAF", + "rules": [] + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "/Users/assafa/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", + "scan", + "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1693477603-3697552683/results.sarif" + ], + "workingDirectory": { + "uri": "file:///Users/assafa/Documents/code/terraform" + } + } + ], + "results": [] + } + ], + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" +} \ No newline at end of file diff --git a/xray/formats/conversion.go b/xray/formats/conversion.go index 88e2c2612..3acb92fbe 100644 --- a/xray/formats/conversion.go +++ b/xray/formats/conversion.go @@ -140,7 +140,7 @@ func ConvertToOperationalRiskViolationScanTableRow(rows []OperationalRiskViolati return } -func ConvertToSecretsTableRow(rows []IacSecretsRow) (tableRows []secretsTableRow) { +func ConvertToSecretsTableRow(rows []SourceCodeRow) (tableRows []secretsTableRow) { for i := range rows { tableRows = append(tableRows, secretsTableRow{ severity: rows[i].Severity, @@ -152,7 +152,7 @@ func ConvertToSecretsTableRow(rows []IacSecretsRow) (tableRows []secretsTableRow return } -func ConvertToIacTableRow(rows []IacSecretsRow) (tableRows []iacTableRow) { +func ConvertToIacTableRow(rows []SourceCodeRow) (tableRows []iacTableRow) { for i := range rows { tableRows = append(tableRows, iacTableRow{ severity: rows[i].Severity, @@ -164,6 +164,18 @@ func ConvertToIacTableRow(rows []IacSecretsRow) (tableRows []iacTableRow) { return } +func ConvertToSastTableRow(rows []SourceCodeRow) (tableRows []sastTableRow) { + for i := range rows { + tableRows = append(tableRows, sastTableRow{ + severity: rows[i].Severity, + file: rows[i].File, + lineColumn: rows[i].LineColumn, + text: rows[i].Text, + }) + } + return +} + func convertToComponentTableRow(rows []ComponentRow) (tableRows []directDependenciesTableRow) { for i := range rows { tableRows = append(tableRows, directDependenciesTableRow{ diff --git a/xray/formats/simplejsonapi.go b/xray/formats/simplejsonapi.go index 54cccdcf8..d56482c25 100644 --- a/xray/formats/simplejsonapi.go +++ b/xray/formats/simplejsonapi.go @@ -12,8 +12,9 @@ type SimpleJsonResults struct { LicensesViolations []LicenseViolationRow `json:"licensesViolations"` Licenses []LicenseRow `json:"licenses"` OperationalRiskViolations []OperationalRiskViolationRow `json:"operationalRiskViolations"` - Secrets []IacSecretsRow `json:"secrets"` - Iacs []IacSecretsRow `json:"iacViolations"` + Secrets []SourceCodeRow `json:"secrets"` + Iacs []SourceCodeRow `json:"iacViolations"` + Sast []SourceCodeRow `json:"sastViolations"` Errors []SimpleJsonError `json:"errors"` } @@ -73,13 +74,18 @@ type OperationalRiskViolationRow struct { LatestVersion string `json:"latestVersion"` } -type IacSecretsRow struct { +type SourceCodeRow struct { Severity string `json:"severity"` SeverityNumValue int `json:"-"` // For sorting - File string `json:"file"` - LineColumn string `json:"lineColumn"` - Text string `json:"text"` - Type string `json:"type"` + SourceCodeLocationRow + Type string `json:"type"` + CodeFlow [][]SourceCodeLocationRow `json:"codeFlow,omitempty"` +} + +type SourceCodeLocationRow struct { + File string `json:"file"` + LineColumn string `json:"lineColumn"` + Text string `json:"text"` } type ComponentRow struct { diff --git a/xray/formats/table.go b/xray/formats/table.go index 44fe79f20..c099b058d 100644 --- a/xray/formats/table.go +++ b/xray/formats/table.go @@ -136,3 +136,10 @@ type iacTableRow struct { lineColumn string `col-name:"Line:Column"` text string `col-name:"Finding"` } + +type sastTableRow struct { + severity string `col-name:"Severity"` + file string `col-name:"File"` + lineColumn string `col-name:"Line:Column"` + text string `col-name:"Finding"` +} diff --git a/xray/utils/analyzermanager.go b/xray/utils/analyzermanager.go index 296c413b3..ad928ecc7 100644 --- a/xray/utils/analyzermanager.go +++ b/xray/utils/analyzermanager.go @@ -7,8 +7,6 @@ import ( "os/exec" "path" "path/filepath" - "strconv" - "strings" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" @@ -16,7 +14,6 @@ import ( "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray/services" - "github.com/owenrumney/go-sarif/v2/sarif" ) type SarifLevel string @@ -41,23 +38,24 @@ var ( ) const ( - EntitlementsMinVersion = "3.66.5" - ApplicabilityFeatureId = "contextual_analysis" - AnalyzerManagerZipName = "analyzerManager.zip" - analyzerManagerVersion = "1.2.4.1953469" - analyzerManagerDownloadPath = "xsc-gen-exe-analyzer-manager-local/v1" - analyzerManagerDirName = "analyzerManager" - analyzerManagerExecutableName = "analyzerManager" - analyzerManagerLogDirName = "analyzerManagerLogs" - jfUserEnvVariable = "JF_USER" - jfPasswordEnvVariable = "JF_PASS" - jfTokenEnvVariable = "JF_TOKEN" - jfPlatformUrlEnvVariable = "JF_PLATFORM_URL" - logDirEnvVariable = "AM_LOG_DIRECTORY" - notEntitledExitCode = 31 - unsupportedCommandExitCode = 13 - unsupportedOsExitCode = 55 - ErrFailedScannerRun = "failed to run %s scan. Exit code received: %s" + EntitlementsMinVersion = "3.66.5" + ApplicabilityFeatureId = "contextual_analysis" + AnalyzerManagerZipName = "analyzerManager.zip" + AnalyzerManagerVersion = "1.2.4.1953469" + MinAnalyzerManagerVersionForSast = "1.3" + analyzerManagerDownloadPath = "xsc-gen-exe-analyzer-manager-local/v1" + analyzerManagerDirName = "analyzerManager" + analyzerManagerExecutableName = "analyzerManager" + analyzerManagerLogDirName = "analyzerManagerLogs" + jfUserEnvVariable = "JF_USER" + jfPasswordEnvVariable = "JF_PASS" + jfTokenEnvVariable = "JF_TOKEN" + jfPlatformUrlEnvVariable = "JF_PLATFORM_URL" + logDirEnvVariable = "AM_LOG_DIRECTORY" + notEntitledExitCode = 31 + unsupportedCommandExitCode = 13 + unsupportedOsExitCode = 55 + ErrFailedScannerRun = "failed to run %s scan. Exit code received: %s" ) const ( @@ -66,15 +64,16 @@ const ( ApplicabilityUndeterminedStringValue = "Undetermined" ) -type ScanType string +type JasScanType string const ( - Applicability ScanType = "Applicability" - Secrets ScanType = "Secrets" - IaC ScanType = "IaC" + Applicability JasScanType = "Applicability" + Secrets JasScanType = "Secrets" + IaC JasScanType = "IaC" + Sast JasScanType = "Sast" ) -func (st ScanType) FormattedError(err error) error { +func (st JasScanType) FormattedError(err error) error { if err != nil { return fmt.Errorf(ErrFailedScannerRun, st, err.Error()) } @@ -87,20 +86,26 @@ var exitCodeErrorsMap = map[int]string{ unsupportedOsExitCode: "got unsupported operating system error from analyzer manager", } -type IacOrSecretResult struct { - Severity string +type SourceCodeLocation struct { File string LineColumn string - Type string Text string } +type SourceCodeScanResult struct { + SourceCodeLocation + Severity string + Type string + CodeFlow [][]SourceCodeLocation +} + type ExtendedScanResults struct { XrayResults []services.ScanResponse ScannedTechnologies []coreutils.Technology ApplicabilityScanResults map[string]string - SecretsScanResults []IacOrSecretResult - IacScanResults []IacOrSecretResult + SecretsScanResults []SourceCodeScanResult + IacScanResults []SourceCodeScanResult + SastResults []SourceCodeScanResult EntitledForJas bool } @@ -112,7 +117,7 @@ type AnalyzerManager struct { AnalyzerManagerFullPath string } -func (am *AnalyzerManager) Exec(configFile, scanCommand string, serverDetails *config.ServerDetails) (err error) { +func (am *AnalyzerManager) Exec(configFile, scanCommand, workingDir string, serverDetails *config.ServerDetails) (err error) { if err = SetAnalyzerManagerEnvVariables(serverDetails); err != nil { return err } @@ -124,7 +129,7 @@ func (am *AnalyzerManager) Exec(configFile, scanCommand string, serverDetails *c } } }() - cmd.Dir = filepath.Dir(am.AnalyzerManagerFullPath) + cmd.Dir = workingDir err = cmd.Run() return errorutils.CheckError(err) } @@ -134,7 +139,7 @@ func GetAnalyzerManagerDownloadPath() (string, error) { if err != nil { return "", err } - return path.Join(analyzerManagerDownloadPath, analyzerManagerVersion, osAndArc, AnalyzerManagerZipName), nil + return path.Join(analyzerManagerDownloadPath, AnalyzerManagerVersion, osAndArc, AnalyzerManagerZipName), nil } func GetAnalyzerManagerDirAbsolutePath() (string, error) { @@ -195,7 +200,7 @@ func SetAnalyzerManagerEnvVariables(serverDetails *config.ServerDetails) error { return nil } -func ParseAnalyzerManagerError(scanner ScanType, err error) error { +func ParseAnalyzerManagerError(scanner JasScanType, err error) error { var exitError *exec.ExitError if errors.As(err, &exitError) { exitCode := exitError.ExitCode() @@ -219,42 +224,6 @@ func RemoveDuplicateValues(stringSlice []string) []string { return finalSlice } -func GetResultFileName(result *sarif.Result) string { - if len(result.Locations) > 0 { - filePath := result.Locations[0].PhysicalLocation.ArtifactLocation.URI - if filePath != nil { - return *filePath - } - } - return "" -} - -func GetResultLocationInFile(result *sarif.Result) string { - if len(result.Locations) > 0 { - startLine := result.Locations[0].PhysicalLocation.Region.StartLine - startColumn := result.Locations[0].PhysicalLocation.Region.StartColumn - if startLine != nil && startColumn != nil { - return strconv.Itoa(*startLine) + ":" + strconv.Itoa(*startColumn) - } - } - return "" -} - -func ExtractRelativePath(resultPath string, projectRoot string) string { - filePrefix := "file://" - relativePath := strings.ReplaceAll(strings.ReplaceAll(resultPath, projectRoot, ""), filePrefix, "") - return relativePath -} - -func GetResultSeverity(result *sarif.Result) string { - if result.Level != nil { - if severity, ok := levelToSeverity[SarifLevel(strings.ToLower(*result.Level))]; ok { - return severity - } - } - return SeverityDefaultValue -} - // Receives a list of relative path working dirs, returns a list of full paths working dirs func GetFullPathsWorkingDirs(workingDirs []string) ([]string, error) { if len(workingDirs) == 0 { diff --git a/xray/utils/analyzermanager_test.go b/xray/utils/analyzermanager_test.go index 5d3d23e08..8445d4c9e 100644 --- a/xray/utils/analyzermanager_test.go +++ b/xray/utils/analyzermanager_test.go @@ -147,7 +147,7 @@ func TestGetResultSeverity(t *testing.T) { func TestScanTypeErrorMsg(t *testing.T) { tests := []struct { - scanner ScanType + scanner JasScanType err error wantMsg string }{ diff --git a/xray/utils/resultstable.go b/xray/utils/resultstable.go index dc031842b..da607d17d 100644 --- a/xray/utils/resultstable.go +++ b/xray/utils/resultstable.go @@ -2,15 +2,16 @@ package utils import ( "fmt" - "github.com/jfrog/gofrog/datastructures" - "golang.org/x/exp/maps" - "golang.org/x/text/cases" - "golang.org/x/text/language" "os" "sort" "strconv" "strings" + "github.com/jfrog/gofrog/datastructures" + "golang.org/x/exp/maps" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "github.com/jfrog/jfrog-cli-core/v2/xray/formats" "github.com/gookit/color" @@ -283,22 +284,24 @@ func PrepareLicenses(licenses []services.License) ([]formats.LicenseRow, error) } // Prepare secrets for all non-table formats (without style or emoji) -func PrepareSecrets(secrets []IacOrSecretResult) []formats.IacSecretsRow { +func PrepareSecrets(secrets []SourceCodeScanResult) []formats.SourceCodeRow { return prepareSecrets(secrets, false) } -func prepareSecrets(secrets []IacOrSecretResult, isTable bool) []formats.IacSecretsRow { - var secretsRows []formats.IacSecretsRow +func prepareSecrets(secrets []SourceCodeScanResult, isTable bool) []formats.SourceCodeRow { + var secretsRows []formats.SourceCodeRow for _, secret := range secrets { currSeverity := GetSeverity(secret.Severity, ApplicableStringValue) secretsRows = append(secretsRows, - formats.IacSecretsRow{ + formats.SourceCodeRow{ Severity: currSeverity.printableTitle(isTable), SeverityNumValue: currSeverity.numValue, - File: secret.File, - LineColumn: secret.LineColumn, - Text: secret.Text, - Type: secret.Type, + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: secret.File, + LineColumn: secret.LineColumn, + Text: secret.Text, + }, + Type: secret.Type, }, ) } @@ -310,7 +313,7 @@ func prepareSecrets(secrets []IacOrSecretResult, isTable bool) []formats.IacSecr return secretsRows } -func PrintSecretsTable(secrets []IacOrSecretResult, entitledForSecretsScan bool) error { +func PrintSecretsTable(secrets []SourceCodeScanResult, entitledForSecretsScan bool) error { if entitledForSecretsScan { secretsRows := prepareSecrets(secrets, true) log.Output() @@ -321,22 +324,24 @@ func PrintSecretsTable(secrets []IacOrSecretResult, entitledForSecretsScan bool) } // Prepare iacs for all non-table formats (without style or emoji) -func PrepareIacs(iacs []IacOrSecretResult) []formats.IacSecretsRow { +func PrepareIacs(iacs []SourceCodeScanResult) []formats.SourceCodeRow { return prepareIacs(iacs, false) } -func prepareIacs(iacs []IacOrSecretResult, isTable bool) []formats.IacSecretsRow { - var iacRows []formats.IacSecretsRow +func prepareIacs(iacs []SourceCodeScanResult, isTable bool) []formats.SourceCodeRow { + var iacRows []formats.SourceCodeRow for _, iac := range iacs { currSeverity := GetSeverity(iac.Severity, ApplicableStringValue) iacRows = append(iacRows, - formats.IacSecretsRow{ + formats.SourceCodeRow{ Severity: currSeverity.printableTitle(isTable), SeverityNumValue: currSeverity.numValue, - File: iac.File, - LineColumn: iac.LineColumn, - Text: iac.Text, - Type: iac.Type, + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: iac.File, + LineColumn: iac.LineColumn, + Text: iac.Text, + }, + Type: iac.Type, }, ) } @@ -348,7 +353,7 @@ func prepareIacs(iacs []IacOrSecretResult, isTable bool) []formats.IacSecretsRow return iacRows } -func PrintIacTable(iacs []IacOrSecretResult, entitledForIacScan bool) error { +func PrintIacTable(iacs []SourceCodeScanResult, entitledForIacScan bool) error { if entitledForIacScan { iacRows := prepareIacs(iacs, true) log.Output() @@ -358,6 +363,65 @@ func PrintIacTable(iacs []IacOrSecretResult, entitledForIacScan bool) error { return nil } +func PrepareSast(sasts []SourceCodeScanResult) []formats.SourceCodeRow { + return prepareSast(sasts, false) +} + +func prepareSast(sasts []SourceCodeScanResult, isTable bool) []formats.SourceCodeRow { + var sastRows []formats.SourceCodeRow + for _, sast := range sasts { + currSeverity := GetSeverity(sast.Severity, ApplicableStringValue) + sastRows = append(sastRows, + formats.SourceCodeRow{ + Severity: currSeverity.printableTitle(isTable), + SeverityNumValue: currSeverity.numValue, + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: sast.File, + LineColumn: sast.LineColumn, + Text: sast.Text, + }, + Type: sast.Type, + CodeFlow: toSourceCodeCodeFlowRow(sast, isTable), + }, + ) + } + + sort.Slice(sastRows, func(i, j int) bool { + return sastRows[i].SeverityNumValue > sastRows[j].SeverityNumValue + }) + + return sastRows +} + +func toSourceCodeCodeFlowRow(result SourceCodeScanResult, isTable bool) (flows [][]formats.SourceCodeLocationRow) { + if isTable { + // Not displaying in table + return + } + for _, flowStack := range result.CodeFlow { + rowFlow := []formats.SourceCodeLocationRow{} + for _, location := range flowStack { + rowFlow = append(rowFlow, formats.SourceCodeLocationRow{ + File: location.File, + LineColumn: location.LineColumn, + Text: location.Text, + }) + } + flows = append(flows, rowFlow) + } + return +} + +func PrintSastTable(sast []SourceCodeScanResult, entitledForSastScan bool) error { + if entitledForSastScan { + sastRows := prepareSast(sast, true) + log.Output() + return coreutils.PrintTable(formats.ConvertToSastTableRow(sastRows), "Static Application Security Testing (SAST)", + "✨ No Static Application Security Testing vulnerabilities were found ✨", false) + } + return nil +} + func convertCves(cves []services.Cve) []formats.CveRow { var cveRows []formats.CveRow for _, cveObj := range cves { diff --git a/xray/utils/resultwriter.go b/xray/utils/resultwriter.go index a1942948a..768229304 100644 --- a/xray/utils/resultwriter.go +++ b/xray/utils/resultwriter.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/jfrog/gofrog/version" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/formats" clientUtils "github.com/jfrog/jfrog-client-go/utils" @@ -45,7 +46,8 @@ type sarifProperties struct { XrayID string File string LineColumn string - SecretsOrIacType string + Type string + CodeFlows [][]formats.SourceCodeLocationRow } // PrintScanResults prints the scan results in the specified format. @@ -109,7 +111,13 @@ func printScanResultsTables(results *ExtendedScanResults, isBinaryScan, includeV if err = PrintSecretsTable(results.SecretsScanResults, results.EntitledForJas); err != nil { return } - return PrintIacTable(results.IacScanResults, results.EntitledForJas) + if err = PrintIacTable(results.IacScanResults, results.EntitledForJas); err != nil { + return + } + if !version.NewVersion(AnalyzerManagerVersion).AtLeast(MinAnalyzerManagerVersionForSast) { + return + } + return PrintSastTable(results.SastResults, results.EntitledForJas) } func printMessages(messages []string) { @@ -170,6 +178,10 @@ func convertScanToSimpleJson(extendedResults *ExtendedScanResults, errors []form iacRows := PrepareIacs(extendedResults.IacScanResults) jsonTable.Iacs = iacRows } + if len(extendedResults.SastResults) > 0 { + sastRows := PrepareSast(extendedResults.SastResults) + jsonTable.Sast = sastRows + } if includeLicenses { licJsonTable, err := PrepareLicenses(licenses) if err != nil { @@ -193,7 +205,7 @@ func convertScanToSarif(run *sarif.Run, extendedResults *ExtendedScanResults, is return err } } - return convertToIacOrSecretsSarif(run, &jsonTable, markdownOutput) + return convertToSourceCodeResultSarif(run, &jsonTable, markdownOutput) } func convertToVulnerabilityOrViolationSarif(run *sarif.Run, jsonTable *formats.SimpleJsonResults, markdownOutput bool) error { @@ -203,26 +215,32 @@ func convertToVulnerabilityOrViolationSarif(run *sarif.Run, jsonTable *formats.S return convertVulnerabilitiesToSarif(jsonTable, run, markdownOutput) } -func convertToIacOrSecretsSarif(run *sarif.Run, jsonTable *formats.SimpleJsonResults, markdownOutput bool) error { - var err error +func convertToSourceCodeResultSarif(run *sarif.Run, jsonTable *formats.SimpleJsonResults, markdownOutput bool) (err error) { for _, secret := range jsonTable.Secrets { - properties := getIacOrSecretsProperties(secret, markdownOutput, true) + properties := getSourceCodeProperties(secret, markdownOutput, Secrets) if err = addPropertiesToSarifRun(run, &properties); err != nil { - return err + return } } for _, iac := range jsonTable.Iacs { - properties := getIacOrSecretsProperties(iac, markdownOutput, false) + properties := getSourceCodeProperties(iac, markdownOutput, IaC) if err = addPropertiesToSarifRun(run, &properties); err != nil { - return err + return + } + } + + for _, sast := range jsonTable.Sast { + properties := getSourceCodeProperties(sast, markdownOutput, Sast) + if err = addPropertiesToSarifRun(run, &properties); err != nil { + return } } - return err + return } -func getIacOrSecretsProperties(secretOrIac formats.IacSecretsRow, markdownOutput, isSecret bool) sarifProperties { - file := strings.TrimPrefix(secretOrIac.File, string(os.PathSeparator)) +func getSourceCodeProperties(sourceCodeIssue formats.SourceCodeRow, markdownOutput bool, scanType JasScanType) sarifProperties { + file := strings.TrimPrefix(sourceCodeIssue.File, string(os.PathSeparator)) mapSeverityToScore := map[string]string{ "": "0.0", "unknown": "0.0", @@ -231,28 +249,38 @@ func getIacOrSecretsProperties(secretOrIac formats.IacSecretsRow, markdownOutput "high": "8.9", "critical": "10", } - severity := mapSeverityToScore[strings.ToLower(secretOrIac.Severity)] - markdownDescription := "" - headline := "Infrastructure as Code Vulnerability" - secretOrFinding := "Finding" - if isSecret { - secretOrFinding = "Secret" + severity := mapSeverityToScore[strings.ToLower(sourceCodeIssue.Severity)] + + headline := "" + secretOrFinding := "" + switch scanType { + case IaC: + headline = "Infrastructure as Code Vulnerability" + secretOrFinding = "Finding" + case Sast: + headline = sourceCodeIssue.Text + secretOrFinding = "Finding" + case Secrets: headline = "Potential Secret Exposed" + secretOrFinding = "Secret" } + + markdownDescription := "" if markdownOutput { headerRow := fmt.Sprintf("| Severity | File | Line:Column | %s |\n", secretOrFinding) separatorRow := "| :---: | :---: | :---: | :---: |\n" tableHeader := headerRow + separatorRow - markdownDescription = tableHeader + fmt.Sprintf("| %s | %s | %s | %s |", secretOrIac.Severity, file, secretOrIac.LineColumn, secretOrIac.Text) + markdownDescription = tableHeader + fmt.Sprintf("| %s | %s | %s | %s |", sourceCodeIssue.Severity, file, sourceCodeIssue.LineColumn, sourceCodeIssue.Text) } return sarifProperties{ Headline: headline, Severity: severity, - Description: secretOrIac.Text, + Description: sourceCodeIssue.Text, MarkdownDescription: markdownDescription, File: file, - LineColumn: secretOrIac.LineColumn, - SecretsOrIacType: secretOrIac.Type, + LineColumn: sourceCodeIssue.LineColumn, + Type: sourceCodeIssue.Type, + CodeFlows: sourceCodeIssue.CodeFlow, } } @@ -372,17 +400,13 @@ func addPropertiesToSarifRun(run *sarif.Run, properties *sarifProperties) error 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 - } + location, err := getSarifLocation(properties.File, properties.LineColumn) + if err != nil { + return err + } + codeFlows, err := getCodeFlowProperties(properties) + if err != nil { + return err } ruleID := generateSarifRuleID(properties) run.AddRule(ruleID). @@ -390,20 +414,56 @@ func addPropertiesToSarifRun(run *sarif.Run, properties *sarifProperties) error WithProperties(pb.Properties). WithMarkdownHelp(markdownDescription) run.CreateResultForRule(ruleID). + WithCodeFlows(codeFlows). WithMessage(sarif.NewTextMessage(properties.Headline)). - AddLocation( - sarif.NewLocationWithPhysicalLocation( - sarif.NewPhysicalLocation(). - WithArtifactLocation( - sarif.NewSimpleArtifactLocation(properties.File), - ).WithRegion( - sarif.NewSimpleRegion(line, line). - WithStartColumn(column)), - ), - ) + AddLocation(location) return nil } +func getSarifLocation(file, lineCol string) (location *sarif.Location, err error) { + line := 0 + column := 0 + if lineCol != "" { + lineColumn := strings.Split(lineCol, ":") + if line, err = strconv.Atoi(lineColumn[0]); err != nil { + return + } + if column, err = strconv.Atoi(lineColumn[1]); err != nil { + return + } + } + location = sarif.NewLocationWithPhysicalLocation( + sarif.NewPhysicalLocation(). + WithArtifactLocation( + sarif.NewSimpleArtifactLocation(file), + ).WithRegion( + sarif.NewSimpleRegion(line, line). + WithStartColumn(column)), + ) + return +} + +func getCodeFlowProperties(properties *sarifProperties) (flows []*sarif.CodeFlow, err error) { + for _, codeFlow := range properties.CodeFlows { + if len(codeFlow) == 0 { + continue + } + converted := sarif.NewCodeFlow() + locations := []*sarif.ThreadFlowLocation{} + for _, location := range codeFlow { + var convertedLocation *sarif.Location + if convertedLocation, err = getSarifLocation(location.File, location.LineColumn); err != nil { + return + } + locations = append(locations, sarif.NewThreadFlowLocation().WithLocation(convertedLocation)) + } + + converted.AddThreadFlow(sarif.NewThreadFlow().WithLocations(locations)) + flows = append(flows, converted) + } + return +} + func generateSarifRuleID(properties *sarifProperties) string { switch { case properties.Cves != "": diff --git a/xray/utils/resultwriter_test.go b/xray/utils/resultwriter_test.go index 44deddaba..8a015267c 100644 --- a/xray/utils/resultwriter_test.go +++ b/xray/utils/resultwriter_test.go @@ -2,12 +2,13 @@ package utils import ( "fmt" + "path" + "testing" + "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) { @@ -27,22 +28,26 @@ func TestGenerateSarifFileFromScan(t *testing.T) { }, }, }, - SecretsScanResults: []IacOrSecretResult{ + SecretsScanResults: []SourceCodeScanResult{ { - Severity: "Medium", - File: "found_secrets.js", - LineColumn: "1:18", - Type: "entropy", - Text: "AAA************", + Severity: "Medium", + SourceCodeLocation: SourceCodeLocation{ + File: "found_secrets.js", + LineColumn: "1:18", + Text: "AAA************", + }, + Type: "entropy", }, }, - IacScanResults: []IacOrSecretResult{ + IacScanResults: []SourceCodeScanResult{ { - Severity: "Medium", - File: "plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json", - LineColumn: "229:38", - Type: "entropy", - Text: "BBB************", + Severity: "Medium", + SourceCodeLocation: SourceCodeLocation{ + File: "plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json", + LineColumn: "229:38", + Text: "BBB************", + }, + Type: "entropy", }, }, } @@ -99,22 +104,24 @@ func TestGetCves(t *testing.T) { func TestGetIacOrSecretsProperties(t *testing.T) { testCases := []struct { name string - secretOrIac formats.IacSecretsRow + row formats.SourceCodeRow markdownOutput bool - isSecret bool + isSecret JasScanType 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", + row: formats.SourceCodeRow{ + Severity: "high", + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: path.Join("path", "to", "file"), + LineColumn: "10:5", + Text: "Vulnerable code", + }, + Type: "Terraform", }, markdownOutput: false, - isSecret: false, + isSecret: IaC, expectedOutput: sarifProperties{ Applicable: "", Cves: "", @@ -125,20 +132,22 @@ func TestGetIacOrSecretsProperties(t *testing.T) { XrayID: "", File: path.Join("path", "to", "file"), LineColumn: "10:5", - SecretsOrIacType: "Terraform", + Type: "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", + row: formats.SourceCodeRow{ + Severity: "medium", + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: path.Join("path", "to", "file"), + LineColumn: "5:3", + Text: "Potential secret", + }, + Type: "AWS Secret Manager", }, markdownOutput: true, - isSecret: true, + isSecret: Secrets, expectedOutput: sarifProperties{ Applicable: "", Cves: "", @@ -149,14 +158,14 @@ func TestGetIacOrSecretsProperties(t *testing.T) { XrayID: "", File: path.Join("path", "to", "file"), LineColumn: "5:3", - SecretsOrIacType: "AWS Secret Manager", + Type: "AWS Secret Manager", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - output := getIacOrSecretsProperties(testCase.secretOrIac, testCase.markdownOutput, testCase.isSecret) + output := getSourceCodeProperties(testCase.row, 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) @@ -166,7 +175,7 @@ func TestGetIacOrSecretsProperties(t *testing.T) { 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) + assert.Equal(t, testCase.expectedOutput.Type, output.Type) }) } } diff --git a/xray/utils/sarifutils.go b/xray/utils/sarifutils.go new file mode 100644 index 000000000..1f77ed161 --- /dev/null +++ b/xray/utils/sarifutils.go @@ -0,0 +1,130 @@ +package utils + +import ( + "strconv" + "strings" + + "github.com/owenrumney/go-sarif/v2/sarif" +) + +// If exists SourceCodeScanResult with the same location as the provided SarifResult, return it +func IsSarifResultExistsInSourceCodeScanResults(result *sarif.Result, workingDir string, results *[]SourceCodeScanResult) *SourceCodeScanResult { + file := ExtractRelativePath(GetResultFileName(result), workingDir) + lineCol := GetResultLocationInFile(result) + text := *result.Message.Text + for _, result := range *results { + if result.File == file && result.LineColumn == lineCol && result.Text == text { + return &result + } + } + return nil +} + +func ConvertSarifResultToSourceCodeScanResult(result *sarif.Result, workingDir string, results *[]SourceCodeScanResult) *SourceCodeScanResult { + file := ExtractRelativePath(GetResultFileName(result), workingDir) + lineCol := GetResultLocationInFile(result) + text := *result.Message.Text + + return &SourceCodeScanResult{ + Severity: GetResultSeverity(result), + SourceCodeLocation: SourceCodeLocation{ + File: file, + LineColumn: lineCol, + Text: text, + }, + Type: *result.RuleID, + } +} + +func GetResultCodeFlows(result *sarif.Result, workingDir string) (flows [][]SourceCodeLocation) { + if len(result.CodeFlows) == 0 { + return + } + for _, codeFlow := range result.CodeFlows { + if codeFlow == nil || len(codeFlow.ThreadFlows) == 0 { + continue + } + flows = append(flows, extractThreadFlows(codeFlow.ThreadFlows, workingDir)...) + } + return +} + +func extractThreadFlows(threadFlows []*sarif.ThreadFlow, workingDir string) (flows [][]SourceCodeLocation) { + for _, threadFlow := range threadFlows { + if threadFlow == nil || len(threadFlow.Locations) == 0 { + continue + } + flow := extractStackTraceLocations(threadFlow.Locations, workingDir) + if len(flow) > 0 { + flows = append(flows, flow) + } + } + return +} + +func extractStackTraceLocations(locations []*sarif.ThreadFlowLocation, workingDir string) (flow []SourceCodeLocation) { + for _, location := range locations { + if location == nil { + continue + } + flow = append(flow, SourceCodeLocation{ + File: ExtractRelativePath(getResultFileName(location.Location), workingDir), + LineColumn: getResultLocationInFile(location.Location), + Text: GetResultLocationSnippet(location.Location), + }) + } + return +} + +func GetResultLocationSnippet(location *sarif.Location) string { + if location != nil && location.PhysicalLocation != nil && location.PhysicalLocation.Region != nil && location.PhysicalLocation.Region.Snippet != nil { + return *location.PhysicalLocation.Region.Snippet.Text + } + return "" +} + +func GetResultFileName(result *sarif.Result) string { + if len(result.Locations) > 0 { + return getResultFileName(result.Locations[0]) + } + return "" +} + +func getResultFileName(location *sarif.Location) string { + filePath := location.PhysicalLocation.ArtifactLocation.URI + if filePath != nil { + return *filePath + } + return "" +} + +func GetResultLocationInFile(result *sarif.Result) string { + if len(result.Locations) > 0 { + return getResultLocationInFile(result.Locations[0]) + } + return "" +} + +func getResultLocationInFile(location *sarif.Location) string { + startLine := location.PhysicalLocation.Region.StartLine + startColumn := location.PhysicalLocation.Region.StartColumn + if startLine != nil && startColumn != nil { + return strconv.Itoa(*startLine) + ":" + strconv.Itoa(*startColumn) + } + return "" +} + +func ExtractRelativePath(resultPath string, projectRoot string) string { + filePrefix := "file://" + relativePath := strings.ReplaceAll(strings.ReplaceAll(resultPath, projectRoot, ""), filePrefix, "") + return relativePath +} + +func GetResultSeverity(result *sarif.Result) string { + if result.Level != nil { + if severity, ok := levelToSeverity[SarifLevel(strings.ToLower(*result.Level))]; ok { + return severity + } + } + return SeverityDefaultValue +}