diff --git a/xray/commands/audit/applicabilitymanager.go b/xray/audit/jas/applicabilitymanager.go similarity index 57% rename from xray/commands/audit/applicabilitymanager.go rename to xray/audit/jas/applicabilitymanager.go index 0983584f4..818a66481 100644 --- a/xray/commands/audit/applicabilitymanager.go +++ b/xray/audit/jas/applicabilitymanager.go @@ -1,12 +1,8 @@ -package audit +package jas import ( + "errors" "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" @@ -17,70 +13,54 @@ import ( xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/owenrumney/go-sarif/v2/sarif" "gopkg.in/yaml.v2" + "os" + "path/filepath" + "strings" ) const ( ApplicabilityFeatureId = "contextual_analysis" applicabilityScanType = "analyze-applicability" applicabilityScanFailureMessage = "failed to run applicability scan. Cause: %s" - noEntitledExitCode = 31 + applicabilityScanCommand = "ca" ) var ( - analyzerManagerExecuter utils.AnalyzerManagerInterface = &utils.AnalyzerManager{} - technologiesEligibleForApplicabilityScan = []coreutils.Technology{coreutils.Npm, coreutils.Pip, + technologiesEligibleForApplicabilityScan = []coreutils.Technology{coreutils.Npm, coreutils.Pip, coreutils.Poetry, coreutils.Pipenv, coreutils.Pypi} - skippedDirs = []string{"**/*test*/**", "**/*venv*/**", "**/*node_modules*/**", "**/*target*/**"} ) -func GetExtendedScanResults(results []services.ScanResponse, dependencyTrees []*xrayUtils.GraphNode, - serverDetails *config.ServerDetails) (extendedResults *utils.ExtendedScanResults, err error) { - if err = utils.CreateAnalyzerManagerLogDir(); err != nil { - return - } - applicabilityScanManager, cleanupFunc, err := NewApplicabilityScanManager(results, dependencyTrees, serverDetails) +// The getApplicabilityScanResults function runs the applicability scan flow, which includes the following steps: +// Creating an ApplicabilityScanManager object. +// Checking if the scanned project is eligible for applicability scan. +// Running the analyzer manager executable. +// Parsing the analyzer manager results. +// Return values: +// map[string]string: A map containing the applicability result of each XRAY CVE. +// bool: true if the user is entitled to the applicability scan, false otherwise. +// error: An error object (if any). +func getApplicabilityScanResults(results []services.ScanResponse, dependencyTrees []*xrayUtils.GraphNode, + serverDetails *config.ServerDetails, analyzerManager utils.AnalyzerManagerInterface) (map[string]string, bool, error) { + applicabilityScanManager, cleanupFunc, err := newApplicabilityScanManager(results, dependencyTrees, serverDetails, analyzerManager) if err != nil { - return nil, fmt.Errorf(applicabilityScanFailureMessage, err.Error()) + return nil, false, fmt.Errorf(applicabilityScanFailureMessage, err.Error()) } defer func() { if cleanupFunc != nil { - e := cleanupFunc() - if err == nil { - err = e - } + err = errors.Join(err, cleanupFunc()) } }() - eligibleForApplicabilityScan, err := applicabilityScanManager.eligibleForApplicabilityScan() - if err != nil { - return nil, fmt.Errorf(applicabilityScanFailureMessage, err.Error()) + if !applicabilityScanManager.eligibleForApplicabilityScan() { + log.Debug("The conditions for running the applicability scan are not met. Skipping the execution of the Analyzer Manager") + return nil, false, nil } - if !eligibleForApplicabilityScan { - if len(serverDetails.Url) == 0 { - log.Warn("To include 'Contextual Analysis' information as part of the audit output, please run the 'jf c add' command before running this command.") + if err = applicabilityScanManager.run(); err != nil { + if utils.IsNotEntitledError(err) || utils.IsUnsupportedCommandError(err) { + return nil, false, nil } - log.Debug("The conditions required for running 'Contextual Analysis' as part of the audit are not met.") - return &utils.ExtendedScanResults{XrayResults: results, ApplicabilityScannerResults: nil, EntitledForJas: false}, nil - } - entitledForJas, err := applicabilityScanManager.Run() - if err != nil { - return nil, fmt.Errorf(applicabilityScanFailureMessage, err.Error()) + return nil, true, fmt.Errorf(applicabilityScanFailureMessage, err.Error()) } - if !entitledForJas { - log.Debug("the current user is not entitled for the Advanced Security package") - return &utils.ExtendedScanResults{XrayResults: results, ApplicabilityScannerResults: nil, EntitledForJas: false}, nil - } - applicabilityScanResults := applicabilityScanManager.getApplicabilityScanResults() - extendedScanResults := utils.ExtendedScanResults{XrayResults: results, ApplicabilityScannerResults: applicabilityScanResults, EntitledForJas: true} - return &extendedScanResults, nil -} - -func (a *ApplicabilityScanManager) eligibleForApplicabilityScan() (bool, error) { - analyzerManagerExist, err := a.analyzerManager.ExistLocally() - if err != nil { - return false, err - } - return analyzerManagerExist && resultsIncludeEligibleTechnologies(getXrayVulnerabilities(a.xrayResults), - getXrayViolations(a.xrayResults)) && len(a.serverDetails.Url) > 0, nil + return applicabilityScanManager.applicabilityScanResults, true, nil } // Applicability scan is relevant only to specific programming languages (the languages in this list: @@ -107,18 +87,18 @@ func resultsIncludeEligibleTechnologies(xrayVulnerabilities []services.Vulnerabi } type ApplicabilityScanManager struct { - applicabilityScannerResults map[string]string - xrayResults []services.ScanResponse - xrayDirectVulnerabilities []services.Vulnerability - xrayDirectViolations []services.Violation - configFileName string - resultsFileName string - analyzerManager utils.AnalyzerManagerInterface - serverDetails *config.ServerDetails + applicabilityScanResults map[string]string + xrayVulnerabilities []services.Vulnerability + xrayViolations []services.Violation + xrayResults []services.ScanResponse + configFileName string + resultsFileName string + analyzerManager utils.AnalyzerManagerInterface + serverDetails *config.ServerDetails } -func NewApplicabilityScanManager(xrayScanResults []services.ScanResponse, dependencyTrees []*xrayUtils.GraphNode, - serverDetails *config.ServerDetails) (manager *ApplicabilityScanManager, cleanup func() error, err error) { +func newApplicabilityScanManager(xrayScanResults []services.ScanResponse, dependencyTrees []*xrayUtils.GraphNode, + serverDetails *config.ServerDetails, analyzerManager utils.AnalyzerManagerInterface) (manager *ApplicabilityScanManager, cleanup func() error, err error) { directDependencies := getDirectDependenciesList(dependencyTrees) tempDir, err := fileutils.CreateTempDir() if err != nil { @@ -128,17 +108,21 @@ func NewApplicabilityScanManager(xrayScanResults []services.ScanResponse, depend return fileutils.RemoveTempDir(tempDir) } return &ApplicabilityScanManager{ - applicabilityScannerResults: map[string]string{}, - xrayDirectVulnerabilities: extractXrayDirectVulnerabilities(xrayScanResults, directDependencies), - xrayDirectViolations: extractXrayDirectViolations(xrayScanResults, directDependencies), - xrayResults: xrayScanResults, - configFileName: filepath.Join(tempDir, "config.yaml"), - resultsFileName: filepath.Join(tempDir, "results.sarif"), - analyzerManager: analyzerManagerExecuter, - serverDetails: serverDetails, + applicabilityScanResults: map[string]string{}, + xrayVulnerabilities: extractXrayDirectVulnerabilities(xrayScanResults, directDependencies), + xrayViolations: extractXrayDirectViolations(xrayScanResults, directDependencies), + configFileName: filepath.Join(tempDir, "config.yaml"), + resultsFileName: filepath.Join(tempDir, "results.sarif"), + xrayResults: xrayScanResults, + analyzerManager: analyzerManager, + serverDetails: serverDetails, }, cleanup, nil } +func (a *ApplicabilityScanManager) eligibleForApplicabilityScan() bool { + return resultsIncludeEligibleTechnologies(getXrayVulnerabilities(a.xrayResults), getXrayViolations(a.xrayResults)) +} + // This function gets a liat of xray scan responses that contains direct and indirect violations, and returns only direct // violation of the scanned project, ignoring indirect violations func extractXrayDirectViolations(xrayScanResults []services.ScanResponse, directDependencies []string) []services.Violation { @@ -197,33 +181,27 @@ func getXrayViolations(xrayScanResults []services.ScanResponse) []services.Viola return xrayViolations } -func (a *ApplicabilityScanManager) getApplicabilityScanResults() map[string]string { - return a.applicabilityScannerResults -} - -func (a *ApplicabilityScanManager) Run() (bool, error) { - var err error +func (a *ApplicabilityScanManager) run() (err error) { defer func() { - if a.deleteApplicabilityScanProcessFiles() != nil { - e := a.deleteApplicabilityScanProcessFiles() - if err == nil { - err = e - } + if deleteJasProcessFiles(a.configFileName, a.resultsFileName) != nil { + deleteFilesError := deleteJasProcessFiles(a.configFileName, a.resultsFileName) + err = errors.Join(err, deleteFilesError) } }() if !a.directDependenciesExist() { - return true, nil + return nil } if err = a.createConfigFile(); err != nil { - return true, err - } - if entitledForJas, err := a.runAnalyzerManager(); err != nil { - return entitledForJas, err + return } - if err = a.parseResults(); err != nil { - return true, err + if err = a.runAnalyzerManager(); err != nil { + return } - return true, nil + return a.setScanResults() +} + +func (a *ApplicabilityScanManager) directDependenciesExist() bool { + return len(createCveList(a.xrayVulnerabilities, a.xrayViolations)) > 0 } type applicabilityScanConfig struct { @@ -239,16 +217,12 @@ type scanConfiguration struct { SkippedDirs []string `yaml:"skipped-folders"` } -func (a *ApplicabilityScanManager) directDependenciesExist() bool { - return len(createCveList(a.xrayDirectVulnerabilities, a.xrayDirectViolations)) > 0 -} - func (a *ApplicabilityScanManager) createConfigFile() error { currentDir, err := coreutils.GetWorkingDirectory() if err != nil { return err } - cveWhiteList := utils.RemoveDuplicateValues(createCveList(a.xrayDirectVulnerabilities, a.xrayDirectViolations)) + cveWhiteList := utils.RemoveDuplicateValues(createCveList(a.xrayVulnerabilities, a.xrayViolations)) configFileContent := applicabilityScanConfig{ Scans: []scanConfiguration{ { @@ -262,35 +236,25 @@ func (a *ApplicabilityScanManager) createConfigFile() error { }, } yamlData, err := yaml.Marshal(&configFileContent) - if err != nil { + if errorutils.CheckError(err) != nil { return err } err = os.WriteFile(a.configFileName, yamlData, 0644) - return err + return errorutils.CheckError(err) } -// Runs the analyzerManager app and returns a boolean indicates if the user is entitled for +// Runs the analyzerManager app and returns a boolean to indicate whether the user is entitled for // advance security feature -func (a *ApplicabilityScanManager) runAnalyzerManager() (bool, error) { +func (a *ApplicabilityScanManager) runAnalyzerManager() error { if err := utils.SetAnalyzerManagerEnvVariables(a.serverDetails); err != nil { - return true, err - } - - if err := a.analyzerManager.Exec(a.configFileName); err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - exitCode := exitError.ExitCode() - // User not entitled error - if exitCode == noEntitledExitCode { - return false, err - } - } + return err } - return true, nil + return a.analyzerManager.Exec(a.configFileName, applicabilityScanCommand) } -func (a *ApplicabilityScanManager) parseResults() error { +func (a *ApplicabilityScanManager) setScanResults() error { report, err := sarif.Open(a.resultsFileName) - if err != nil { + if errorutils.CheckError(err) != nil { return err } var fullVulnerabilitiesList []*sarif.Result @@ -298,41 +262,17 @@ func (a *ApplicabilityScanManager) parseResults() error { fullVulnerabilitiesList = report.Runs[0].Results } - xrayCves := utils.RemoveDuplicateValues(createCveList(a.xrayDirectVulnerabilities, a.xrayDirectViolations)) + xrayCves := utils.RemoveDuplicateValues(createCveList(a.xrayVulnerabilities, a.xrayViolations)) for _, xrayCve := range xrayCves { - a.applicabilityScannerResults[xrayCve] = utils.ApplicabilityUndeterminedStringValue + a.applicabilityScanResults[xrayCve] = utils.ApplicabilityUndeterminedStringValue } for _, vulnerability := range fullVulnerabilitiesList { applicableVulnerabilityName := getVulnerabilityName(*vulnerability.RuleID) if isVulnerabilityApplicable(vulnerability) { - a.applicabilityScannerResults[applicableVulnerabilityName] = utils.ApplicableStringValue + a.applicabilityScanResults[applicableVulnerabilityName] = utils.ApplicableStringValue } else { - a.applicabilityScannerResults[applicableVulnerabilityName] = utils.NotApplicableStringValue - } - } - return nil -} - -func (a *ApplicabilityScanManager) deleteApplicabilityScanProcessFiles() error { - exist, err := fileutils.IsFileExists(a.configFileName, false) - if err != nil { - return err - } - if exist { - err = os.Remove(a.configFileName) - if errorutils.CheckError(err) != nil { - return err - } - } - exist, err = fileutils.IsFileExists(a.resultsFileName, false) - if err != nil { - return err - } - if exist { - err = os.Remove(a.resultsFileName) - if errorutils.CheckError(err) != nil { - return err + a.applicabilityScanResults[applicableVulnerabilityName] = utils.NotApplicableStringValue } } return nil diff --git a/xray/commands/audit/applicabilitymanager_test.go b/xray/audit/jas/applicabilitymanager_test.go similarity index 62% rename from xray/commands/audit/applicabilitymanager_test.go rename to xray/audit/jas/applicabilitymanager_test.go index 319d816ac..d7c06cbd6 100644 --- a/xray/commands/audit/applicabilitymanager_test.go +++ b/xray/audit/jas/applicabilitymanager_test.go @@ -1,89 +1,42 @@ -package audit +package jas import ( "errors" - "os" - "path/filepath" - "testing" - - "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "fmt" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/xray/services" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" ) -var ( - analyzerManagerExecutionError error = nil - analyzerManagerExist = true -) - -type analyzerManagerMock struct { -} - -func (am *analyzerManagerMock) Exec(string) error { - return analyzerManagerExecutionError -} - -func (am *analyzerManagerMock) ExistLocally() (bool, error) { - return analyzerManagerExist, nil -} - -var fakeBasicXrayResults = []services.ScanResponse{ - { - ScanId: "scanId_1", - Vulnerabilities: []services.Vulnerability{ - {IssueId: "issueId_1", Technology: coreutils.Pipenv.ToString(), - Cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}, {Id: "testCve3"}}, - Components: map[string]services.Component{"issueId_1_direct_dependency": {}}}, - }, - Violations: []services.Violation{ - {IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), - Cves: []services.Cve{{Id: "testCve4"}, {Id: "testCve5"}}, - Components: map[string]services.Component{"issueId_2_direct_dependency": {}}}, - }, - }, -} - -var fakeBasicDependencyGraph = []*xrayUtils.GraphNode{ - { - Id: "parent_node_id", - Nodes: []*xrayUtils.GraphNode{ - {Id: "issueId_1_direct_dependency", Nodes: []*xrayUtils.GraphNode{{Id: "issueId_1_non_direct_dependency"}}}, - {Id: "issueId_2_direct_dependency", Nodes: nil}, - }, - }, -} - -var fakeServerDetails = config.ServerDetails{ - Url: "platformUrl", - Password: "password", - User: "user", -} - func TestNewApplicabilityScanManager_InputIsValid(t *testing.T) { // Act - applicabilityManager, _, _ := NewApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails) + applicabilityManager, _, err := newApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails, &analyzerManagerMock{}) // Assert + assert.NoError(t, err) assert.NotEmpty(t, applicabilityManager) assert.NotEmpty(t, applicabilityManager.configFileName) assert.NotEmpty(t, applicabilityManager.resultsFileName) - assert.Equal(t, 1, len(applicabilityManager.xrayDirectVulnerabilities)) - assert.Equal(t, 1, len(applicabilityManager.xrayDirectViolations)) + assert.Equal(t, 1, len(applicabilityManager.xrayVulnerabilities)) + assert.Equal(t, 1, len(applicabilityManager.xrayViolations)) } func TestNewApplicabilityScanManager_DependencyTreeDoesntExist(t *testing.T) { // Act - applicabilityManager, _, _ := NewApplicabilityScanManager(fakeBasicXrayResults, nil, &fakeServerDetails) + applicabilityManager, _, err := newApplicabilityScanManager(fakeBasicXrayResults, nil, &fakeServerDetails, &analyzerManagerMock{}) // Assert + assert.NoError(t, err) assert.NotEmpty(t, applicabilityManager) assert.NotEmpty(t, applicabilityManager.configFileName) assert.NotEmpty(t, applicabilityManager.resultsFileName) - assert.Empty(t, applicabilityManager.xrayDirectVulnerabilities) - assert.Empty(t, applicabilityManager.xrayDirectViolations) + assert.Empty(t, applicabilityManager.xrayVulnerabilities) + assert.Empty(t, applicabilityManager.xrayViolations) } func TestNewApplicabilityScanManager_NoDirectDependenciesInTree(t *testing.T) { @@ -111,14 +64,16 @@ func TestNewApplicabilityScanManager_NoDirectDependenciesInTree(t *testing.T) { fakeBasicXrayResults[0].Violations[0].Components["issueId_2_non_direct_dependency"] = services.Component{} // Act - applicabilityManager, _, _ := NewApplicabilityScanManager(noDirectDependenciesResults, fakeBasicDependencyGraph, &fakeServerDetails) + applicabilityManager, _, err := newApplicabilityScanManager(noDirectDependenciesResults, fakeBasicDependencyGraph, &fakeServerDetails, &analyzerManagerMock{}) // Assert + assert.NoError(t, err) assert.NotEmpty(t, applicabilityManager) assert.NotEmpty(t, applicabilityManager.configFileName) assert.NotEmpty(t, applicabilityManager.resultsFileName) - assert.Equal(t, 1, len(applicabilityManager.xrayDirectVulnerabilities)) // non-direct dependency should not be added - assert.Equal(t, 1, len(applicabilityManager.xrayDirectViolations)) // non-direct dependency should not be added + // Non-direct dependencies should not be added + assert.Equal(t, 1, len(applicabilityManager.xrayVulnerabilities)) + assert.Equal(t, 1, len(applicabilityManager.xrayViolations)) } func TestNewApplicabilityScanManager_MultipleDependencyTrees(t *testing.T) { @@ -126,14 +81,15 @@ func TestNewApplicabilityScanManager_MultipleDependencyTrees(t *testing.T) { multipleDependencyTrees := []*xrayUtils.GraphNode{fakeBasicDependencyGraph[0], fakeBasicDependencyGraph[0]} // Act - applicabilityManager, _, _ := NewApplicabilityScanManager(fakeBasicXrayResults, multipleDependencyTrees, &fakeServerDetails) + applicabilityManager, _, err := newApplicabilityScanManager(fakeBasicXrayResults, multipleDependencyTrees, &fakeServerDetails, &analyzerManagerMock{}) // Assert + assert.NoError(t, err) assert.NotEmpty(t, applicabilityManager) assert.NotEmpty(t, applicabilityManager.configFileName) assert.NotEmpty(t, applicabilityManager.resultsFileName) - assert.Equal(t, 2, len(applicabilityManager.xrayDirectVulnerabilities)) - assert.Equal(t, 2, len(applicabilityManager.xrayDirectViolations)) + assert.Equal(t, 2, len(applicabilityManager.xrayVulnerabilities)) + assert.Equal(t, 2, len(applicabilityManager.xrayViolations)) } func TestNewApplicabilityScanManager_ViolationsDontExistInResults(t *testing.T) { @@ -150,14 +106,15 @@ func TestNewApplicabilityScanManager_ViolationsDontExistInResults(t *testing.T) } // Act - applicabilityManager, _, _ := NewApplicabilityScanManager(noViolationScanResponse, fakeBasicDependencyGraph, &fakeServerDetails) + applicabilityManager, _, err := newApplicabilityScanManager(noViolationScanResponse, fakeBasicDependencyGraph, &fakeServerDetails, &analyzerManagerMock{}) // Assert + assert.NoError(t, err) assert.NotEmpty(t, applicabilityManager) assert.NotEmpty(t, applicabilityManager.configFileName) assert.NotEmpty(t, applicabilityManager.resultsFileName) - assert.Equal(t, 1, len(applicabilityManager.xrayDirectVulnerabilities)) - assert.Empty(t, applicabilityManager.xrayDirectViolations) + assert.Equal(t, 1, len(applicabilityManager.xrayVulnerabilities)) + assert.Empty(t, applicabilityManager.xrayViolations) } func TestNewApplicabilityScanManager_VulnerabilitiesDontExist(t *testing.T) { @@ -174,61 +131,62 @@ func TestNewApplicabilityScanManager_VulnerabilitiesDontExist(t *testing.T) { } // Act - applicabilityManager, _, _ := NewApplicabilityScanManager(noVulnerabilitiesScanResponse, fakeBasicDependencyGraph, &fakeServerDetails) + applicabilityManager, _, err := newApplicabilityScanManager(noVulnerabilitiesScanResponse, fakeBasicDependencyGraph, &fakeServerDetails, &analyzerManagerMock{}) // Assert + assert.NoError(t, err) assert.NotEmpty(t, applicabilityManager) assert.NotEmpty(t, applicabilityManager.configFileName) assert.NotEmpty(t, applicabilityManager.resultsFileName) - assert.Equal(t, 1, len(applicabilityManager.xrayDirectViolations)) - assert.Empty(t, applicabilityManager.xrayDirectVulnerabilities) + assert.Equal(t, 1, len(applicabilityManager.xrayViolations)) + assert.Empty(t, applicabilityManager.xrayVulnerabilities) } -func TestApplicabilityScanManager_EligibleForApplicabilityScan_AllConditionsMet(t *testing.T) { +func TestApplicabilityScanManager_ShouldRun_AllConditionsMet(t *testing.T) { // Arrange analyzerManagerExecuter = &analyzerManagerMock{} - applicabilityManager, _, _ := NewApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails) + applicabilityManager, _, err := newApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails, &analyzerManagerMock{}) // Act - eligible, _ := applicabilityManager.eligibleForApplicabilityScan() + eligible := applicabilityManager.eligibleForApplicabilityScan() // Assert + assert.NoError(t, err) assert.True(t, eligible) } -func TestApplicabilityScanManager_EligibleForApplicabilityScan_AnalyzerManagerDoesntExist(t *testing.T) { +func TestApplicabilityScanManager_ShouldRun_TechnologiesNotEligibleForScan(t *testing.T) { + defer func() { + fakeBasicXrayResults[0].Vulnerabilities[0].Technology = coreutils.Pipenv.ToString() + fakeBasicXrayResults[0].Violations[0].Technology = coreutils.Pipenv.ToString() + }() + // Arrange - analyzerManagerExist = false analyzerManagerExecuter = &analyzerManagerMock{} - applicabilityManager, _, _ := NewApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails) + fakeBasicXrayResults[0].Vulnerabilities[0].Technology = coreutils.Nuget.ToString() + fakeBasicXrayResults[0].Violations[0].Technology = coreutils.Go.ToString() + applicabilityManager, _, err := newApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, + &fakeServerDetails, &analyzerManagerMock{}) // Act - eligible, _ := applicabilityManager.eligibleForApplicabilityScan() + eligible := applicabilityManager.eligibleForApplicabilityScan() // Assert + assert.NoError(t, err) assert.False(t, eligible) - - // Cleanup - analyzerManagerExist = true } -func TestApplicabilityScanManager_EligibleForApplicabilityScan_TechnologiesNotEligibleForScan(t *testing.T) { +func TestApplicabilityScanManager_ShouldRun_ScanResultsAreEmpty(t *testing.T) { // Arrange analyzerManagerExecuter = &analyzerManagerMock{} - fakeBasicXrayResults[0].Vulnerabilities[0].Technology = coreutils.Nuget.ToString() - fakeBasicXrayResults[0].Violations[0].Technology = coreutils.Go.ToString() - applicabilityManager, _, _ := NewApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, - &fakeServerDetails) + applicabilityManager, _, err := newApplicabilityScanManager(nil, fakeBasicDependencyGraph, &fakeServerDetails, &analyzerManagerMock{}) // Act - eligible, _ := applicabilityManager.eligibleForApplicabilityScan() + eligible := applicabilityManager.eligibleForApplicabilityScan() // Assert + assert.NoError(t, err) assert.False(t, eligible) - - // Cleanup - fakeBasicXrayResults[0].Vulnerabilities[0].Technology = coreutils.Pipenv.ToString() - fakeBasicXrayResults[0].Violations[0].Technology = coreutils.Pipenv.ToString() } func TestResultsIncludeEligibleTechnologies(t *testing.T) { @@ -274,7 +232,8 @@ func TestExtractXrayDirectViolations(t *testing.T) { Components: map[string]services.Component{"issueId_2_direct_dependency": {}}}, }, }, - {directDependencies: []string{"issueId_1_direct_dependency"}, // vulnerability dependency, should be ignored by function + // Vulnerability dependency, should be ignored by function + {directDependencies: []string{"issueId_1_direct_dependency"}, expectedResult: []services.Violation{}, }, {directDependencies: []string{}, @@ -366,37 +325,41 @@ func TestGetDirectDependenciesList(t *testing.T) { func TestCreateConfigFile_VerifyFileWasCreated(t *testing.T) { // Arrange analyzerManagerExecuter = &analyzerManagerMock{} - applicabilityManager, _, _ := NewApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails) + applicabilityManager, _, applicabilityManagerError := newApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails, &analyzerManagerMock{}) // Act err := applicabilityManager.createConfigFile() + defer func() { + err = os.Remove(applicabilityManager.configFileName) + assert.NoError(t, err) + }() + // Assert + assert.NoError(t, applicabilityManagerError) assert.NoError(t, err) _, fileNotExistError := os.Stat(applicabilityManager.configFileName) assert.NoError(t, fileNotExistError) - fileContent, _ := os.ReadFile(applicabilityManager.configFileName) - assert.True(t, len(fileContent) > 0) - - // Cleanup - err = os.Remove(applicabilityManager.configFileName) + fileContent, err := os.ReadFile(applicabilityManager.configFileName) assert.NoError(t, err) + assert.True(t, len(fileContent) > 0) } func TestParseResults_EmptyResults_AllCvesShouldGetUnknown(t *testing.T) { // Arrange analyzerManagerExecuter = &analyzerManagerMock{} - applicabilityManager, _, _ := NewApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails) - applicabilityManager.resultsFileName = filepath.Join("..", "testdata", "applicability-scan", "empty-results.sarif") + applicabilityManager, _, applicabilityManagerError := newApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails, &analyzerManagerMock{}) + applicabilityManager.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "applicability-scan", "empty-results.sarif") // Act - err := applicabilityManager.parseResults() + err := applicabilityManager.setScanResults() // Assert + assert.NoError(t, applicabilityManagerError) assert.NoError(t, err) - assert.NotEmpty(t, applicabilityManager.applicabilityScannerResults) - assert.Equal(t, 5, len(applicabilityManager.applicabilityScannerResults)) - for _, cveResult := range applicabilityManager.applicabilityScannerResults { + assert.NotEmpty(t, applicabilityManager.applicabilityScanResults) + assert.Equal(t, 5, len(applicabilityManager.applicabilityScanResults)) + for _, cveResult := range applicabilityManager.applicabilityScanResults { assert.Equal(t, utils.ApplicabilityUndeterminedStringValue, cveResult) } } @@ -404,58 +367,44 @@ func TestParseResults_EmptyResults_AllCvesShouldGetUnknown(t *testing.T) { func TestParseResults_ApplicableCveExist(t *testing.T) { // Arrange analyzerManagerExecuter = &analyzerManagerMock{} - applicabilityManager, _, _ := NewApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails) - applicabilityManager.resultsFileName = filepath.Join("..", "testdata", "applicability-scan", "applicable-cve-results.sarif") + applicabilityManager, _, applicabilityManagerError := newApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails, &analyzerManagerMock{}) + applicabilityManager.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "applicability-scan", "applicable-cve-results.sarif") // Act - err := applicabilityManager.parseResults() + err := applicabilityManager.setScanResults() // Assert + assert.NoError(t, applicabilityManagerError) assert.NoError(t, err) - assert.NotEmpty(t, applicabilityManager.applicabilityScannerResults) - assert.Equal(t, 5, len(applicabilityManager.applicabilityScannerResults)) - assert.Equal(t, utils.ApplicableStringValue, applicabilityManager.applicabilityScannerResults["testCve1"]) - assert.Equal(t, utils.NotApplicableStringValue, applicabilityManager.applicabilityScannerResults["testCve3"]) - + assert.NotEmpty(t, applicabilityManager.applicabilityScanResults) + assert.Equal(t, 5, len(applicabilityManager.applicabilityScanResults)) + assert.Equal(t, utils.ApplicableStringValue, applicabilityManager.applicabilityScanResults["testCve1"]) + assert.Equal(t, utils.NotApplicableStringValue, applicabilityManager.applicabilityScanResults["testCve3"]) } func TestParseResults_AllCvesNotApplicable(t *testing.T) { // Arrange analyzerManagerExecuter = &analyzerManagerMock{} - applicabilityManager, _, _ := NewApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails) - applicabilityManager.resultsFileName = filepath.Join("..", "testdata", "applicability-scan", "no-applicable-cves-results.sarif") + applicabilityManager, _, applicabilityManagerError := newApplicabilityScanManager(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails, &analyzerManagerMock{}) + applicabilityManager.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "applicability-scan", "no-applicable-cves-results.sarif") // Act - err := applicabilityManager.parseResults() + err := applicabilityManager.setScanResults() // Assert + assert.NoError(t, applicabilityManagerError) assert.NoError(t, err) - assert.NotEmpty(t, applicabilityManager.applicabilityScannerResults) - assert.Equal(t, 5, len(applicabilityManager.applicabilityScannerResults)) - for _, cveResult := range applicabilityManager.applicabilityScannerResults { + assert.NotEmpty(t, applicabilityManager.applicabilityScanResults) + assert.Equal(t, 5, len(applicabilityManager.applicabilityScanResults)) + for _, cveResult := range applicabilityManager.applicabilityScanResults { assert.Equal(t, utils.NotApplicableStringValue, cveResult) } } -func TestGetExtendedScanResults_AnalyzerManagerDoesntExist(t *testing.T) { - // Arrange - analyzerManagerExist = false - analyzerManagerExecuter = &analyzerManagerMock{} - - // Act - extendedResults, err := GetExtendedScanResults(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails) - - // Assert - assert.NoError(t, err) - assert.False(t, extendedResults.EntitledForJas) - assert.Equal(t, 1, len(extendedResults.XrayResults)) - assert.Nil(t, extendedResults.ApplicabilityScannerResults) - - // Cleanup - analyzerManagerExist = true -} - func TestGetExtendedScanResults_AnalyzerManagerReturnsError(t *testing.T) { + defer func() { + analyzerManagerExecutionError = nil + }() // Arrange analyzerManagerErrorMessage := "analyzer manager failure message" analyzerManagerExecutionError = errors.New(analyzerManagerErrorMessage) @@ -466,8 +415,6 @@ func TestGetExtendedScanResults_AnalyzerManagerReturnsError(t *testing.T) { // Assert assert.Error(t, err) + assert.Equal(t, fmt.Sprintf(applicabilityScanFailureMessage, analyzerManagerErrorMessage), err.Error()) assert.Nil(t, extendedResults) - - // Cleanup - analyzerManagerExecutionError = nil } diff --git a/xray/audit/jas/iacscanner.go b/xray/audit/jas/iacscanner.go new file mode 100644 index 000000000..291fa1452 --- /dev/null +++ b/xray/audit/jas/iacscanner.go @@ -0,0 +1,160 @@ +package jas + +import ( + "errors" + "fmt" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/owenrumney/go-sarif/v2/sarif" + "gopkg.in/yaml.v2" + "os" + "path/filepath" +) + +const ( + iacScannerType = "iac-scan-modules" + iacScanFailureMessage = "failed to run Infrastructure as Code scan. Cause: %s" + iacScanCommand = "iac" +) + +type IacScanManager struct { + iacScannerResults []utils.IacOrSecretResult + configFileName string + resultsFileName string + analyzerManager utils.AnalyzerManagerInterface + serverDetails *config.ServerDetails + projectRootPath string +} + +// The getIacScanResults function runs the iac scan flow, which includes the following steps: +// Creating an IacScanManager object. +// Running the analyzer manager executable. +// Parsing the analyzer manager results. +// Return values: +// []utils.IacOrSecretResult: 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(serverDetails *config.ServerDetails, analyzerManager utils.AnalyzerManagerInterface) ([]utils.IacOrSecretResult, + bool, error) { + iacScanManager, cleanupFunc, err := newIacScanManager(serverDetails, analyzerManager) + if err != nil { + return nil, false, fmt.Errorf(iacScanFailureMessage, err.Error()) + } + defer func() { + if cleanupFunc != nil { + err = errors.Join(err, cleanupFunc()) + } + }() + if err = iacScanManager.run(); err != nil { + if utils.IsNotEntitledError(err) || utils.IsUnsupportedCommandError(err) { + return nil, false, nil + } + return nil, true, fmt.Errorf(iacScanFailureMessage, err.Error()) + } + return iacScanManager.iacScannerResults, true, nil +} + +func newIacScanManager(serverDetails *config.ServerDetails, analyzerManager utils.AnalyzerManagerInterface) (manager *IacScanManager, + cleanup func() error, err error) { + tempDir, err := fileutils.CreateTempDir() + if err != nil { + return + } + cleanup = func() error { + return fileutils.RemoveTempDir(tempDir) + } + return &IacScanManager{ + iacScannerResults: []utils.IacOrSecretResult{}, + configFileName: filepath.Join(tempDir, "config.yaml"), + resultsFileName: filepath.Join(tempDir, "results.sarif"), + analyzerManager: analyzerManager, + serverDetails: serverDetails, + }, cleanup, nil +} + +func (iac *IacScanManager) run() (err error) { + defer func() { + if deleteJasProcessFiles(iac.configFileName, iac.resultsFileName) != nil { + deleteFilesError := deleteJasProcessFiles(iac.configFileName, iac.resultsFileName) + err = errors.Join(err, deleteFilesError) + } + }() + if err = iac.createConfigFile(); err != nil { + return + } + if err = iac.runAnalyzerManager(); err != nil { + return + } + return iac.setScanResults() +} + +type iacScanConfig struct { + Scans []iacScanConfiguration `yaml:"scans"` +} + +type iacScanConfiguration struct { + Roots []string `yaml:"roots"` + Output string `yaml:"output"` + Type string `yaml:"type"` + SkippedDirs []string `yaml:"skipped-folders"` +} + +func (iac *IacScanManager) createConfigFile() error { + currentDir, err := coreutils.GetWorkingDirectory() + if err != nil { + return err + } + iac.projectRootPath = currentDir + configFileContent := iacScanConfig{ + Scans: []iacScanConfiguration{ + { + Roots: []string{currentDir}, + Output: iac.resultsFileName, + Type: iacScannerType, + SkippedDirs: skippedDirs, + }, + }, + } + yamlData, err := yaml.Marshal(&configFileContent) + if errorutils.CheckError(err) != nil { + return err + } + err = os.WriteFile(iac.configFileName, yamlData, 0644) + return errorutils.CheckError(err) +} + +func (iac *IacScanManager) runAnalyzerManager() error { + if err := utils.SetAnalyzerManagerEnvVariables(iac.serverDetails); err != nil { + return err + } + return iac.analyzerManager.Exec(iac.configFileName, iacScanCommand) +} + +func (iac *IacScanManager) setScanResults() error { + report, err := sarif.Open(iac.resultsFileName) + if errorutils.CheckError(err) != nil { + return err + } + var iacResults []*sarif.Result + if len(report.Runs) > 0 { + iacResults = report.Runs[0].Results + } + + finalIacList := []utils.IacOrSecretResult{} + + for _, result := range iacResults { + newIac := utils.IacOrSecretResult{ + Severity: utils.GetResultSeverity(result), + File: utils.ExtractRelativePath(utils.GetResultFileName(result), iac.projectRootPath), + LineColumn: utils.GetResultLocationInFile(result), + Text: *result.Message.Text, + Type: *result.RuleID, + } + finalIacList = append(finalIacList, newIac) + } + iac.iacScannerResults = finalIacList + return nil +} diff --git a/xray/audit/jas/iacscanner_test.go b/xray/audit/jas/iacscanner_test.go new file mode 100644 index 000000000..7377e1045 --- /dev/null +++ b/xray/audit/jas/iacscanner_test.go @@ -0,0 +1,71 @@ +package jas + +import ( + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +func TestNewIacScanManager(t *testing.T) { + // Act + iacScanManager, _, err := newIacScanManager(&fakeServerDetails, &analyzerManagerMock{}) + + // Assert + assert.NoError(t, err) + assert.NotEmpty(t, iacScanManager) + assert.NotEmpty(t, iacScanManager.configFileName) + assert.NotEmpty(t, iacScanManager.resultsFileName) + assert.Equal(t, &fakeServerDetails, iacScanManager.serverDetails) +} + +func TestIacScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { + // Arrange + iacScanManager, _, iacManagerError := newIacScanManager(&fakeServerDetails, &analyzerManagerMock{}) + + // Act + err := iacScanManager.createConfigFile() + + defer func() { + err = os.Remove(iacScanManager.configFileName) + assert.NoError(t, err) + }() + + // Assert + assert.NoError(t, iacManagerError) + assert.NoError(t, err) + _, fileNotExistError := os.Stat(iacScanManager.configFileName) + assert.NoError(t, fileNotExistError) + fileContent, err := os.ReadFile(iacScanManager.configFileName) + assert.NoError(t, err) + assert.True(t, len(fileContent) > 0) +} + +func TestIacParseResults_EmptyResults(t *testing.T) { + // Arrange + iacScanManager, _, iacManagerError := newIacScanManager(&fakeServerDetails, &analyzerManagerMock{}) + iacScanManager.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "iac-scan", "no-violations.sarif") + + // Act + err := iacScanManager.setScanResults() + + // Assert + assert.NoError(t, iacManagerError) + assert.NoError(t, err) + assert.Empty(t, iacScanManager.iacScannerResults) +} + +func TestIacParseResults_ResultsContainSecrets(t *testing.T) { + // Arrange + iacScanManager, _, iacManagerError := newIacScanManager(&fakeServerDetails, &analyzerManagerMock{}) + iacScanManager.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "iac-scan", "contains-iac-violations.sarif") + + // Act + err := iacScanManager.setScanResults() + + // Assert + assert.NoError(t, iacManagerError) + assert.NoError(t, err) + assert.NotEmpty(t, iacScanManager.iacScannerResults) + assert.Equal(t, 4, len(iacScanManager.iacScannerResults)) +} diff --git a/xray/audit/jas/jasmanager.go b/xray/audit/jas/jasmanager.go new file mode 100644 index 000000000..d24ca8beb --- /dev/null +++ b/xray/audit/jas/jasmanager.go @@ -0,0 +1,85 @@ +package jas + +import ( + "errors" + "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" + "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" + xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" + "os" +) + +const serverDetailsErrorMessage = "cant get xray server details" + +var ( + analyzerManagerExecuter utils.AnalyzerManagerInterface = &utils.AnalyzerManager{} + skippedDirs = []string{"**/*test*/**", "**/*venv*/**", "**/*node_modules*/**", "**/*target*/**"} +) + +func GetExtendedScanResults(xrayResults []services.ScanResponse, dependencyTrees []*xrayUtils.GraphNode, + serverDetails *config.ServerDetails) (*utils.ExtendedScanResults, error) { + if serverDetails == nil { + return nil, errors.New(serverDetailsErrorMessage) + } + if len(serverDetails.Url) == 0 { + log.Warn("To include 'Contextual Analysis' information as part of the audit output, please run the 'jf c add' command before running this command.") + return &utils.ExtendedScanResults{XrayResults: xrayResults}, nil + } + analyzerManagerExist, err := analyzerManagerExecuter.ExistLocally() + if err != nil { + return nil, err + } + if !analyzerManagerExist { + log.Debug("Since the 'Analyzer Manager' doesn't exist locally, its execution is skipped.") + return &utils.ExtendedScanResults{XrayResults: xrayResults}, nil + } + if err = utils.CreateAnalyzerManagerLogDir(); err != nil { + return nil, err + } + applicabilityScanResults, eligibleForApplicabilityScan, err := getApplicabilityScanResults(xrayResults, + dependencyTrees, serverDetails, analyzerManagerExecuter) + if err != nil { + return nil, err + } + secretsScanResults, eligibleForSecretsScan, err := getSecretsScanResults(serverDetails, analyzerManagerExecuter) + if err != nil { + return nil, err + } + iacScanResults, eligibleForIacScan, err := getIacScanResults(serverDetails, analyzerManagerExecuter) + if err != nil { + return nil, err + } + return &utils.ExtendedScanResults{ + XrayResults: xrayResults, + ApplicabilityScanResults: applicabilityScanResults, + SecretsScanResults: secretsScanResults, + IacScanResults: iacScanResults, + EntitledForJas: true, + EligibleForApplicabilityScan: eligibleForApplicabilityScan, + EligibleForSecretScan: eligibleForSecretsScan, + EligibleForIacScan: eligibleForIacScan, + }, nil +} + +func deleteJasProcessFiles(configFile string, resultFile string) error { + exist, err := fileutils.IsFileExists(configFile, false) + if err != nil { + return err + } + if exist { + if err = os.Remove(configFile); err != nil { + return errorutils.CheckError(err) + } + } + exist, err = fileutils.IsFileExists(resultFile, false) + if err != nil { + return err + } + if exist { + err = os.Remove(resultFile) + } + return errorutils.CheckError(err) +} diff --git a/xray/audit/jas/jasmanager_test.go b/xray/audit/jas/jasmanager_test.go new file mode 100644 index 000000000..9fd56367b --- /dev/null +++ b/xray/audit/jas/jasmanager_test.go @@ -0,0 +1,83 @@ +package jas + +import ( + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-client-go/xray/services" + xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" + "github.com/stretchr/testify/assert" + "testing" +) + +var ( + analyzerManagerExecutionError error = nil + analyzerManagerExists = true +) + +type analyzerManagerMock struct { +} + +func (am *analyzerManagerMock) Exec(string, string) error { + return analyzerManagerExecutionError +} + +func (am *analyzerManagerMock) ExistLocally() (bool, error) { + return analyzerManagerExists, nil +} + +var fakeBasicXrayResults = []services.ScanResponse{ + { + ScanId: "scanId_1", + Vulnerabilities: []services.Vulnerability{ + {IssueId: "issueId_1", Technology: coreutils.Pipenv.ToString(), + Cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}, {Id: "testCve3"}}, + Components: map[string]services.Component{"issueId_1_direct_dependency": {}}}, + }, + Violations: []services.Violation{ + {IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), + Cves: []services.Cve{{Id: "testCve4"}, {Id: "testCve5"}}, + Components: map[string]services.Component{"issueId_2_direct_dependency": {}}}, + }, + }, +} + +var fakeBasicDependencyGraph = []*xrayUtils.GraphNode{ + { + Id: "parent_node_id", + Nodes: []*xrayUtils.GraphNode{ + {Id: "issueId_1_direct_dependency", Nodes: []*xrayUtils.GraphNode{{Id: "issueId_1_non_direct_dependency"}}}, + {Id: "issueId_2_direct_dependency", Nodes: nil}, + }, + }, +} + +var fakeServerDetails = config.ServerDetails{ + Url: "platformUrl", + Password: "password", + User: "user", +} + +func TestGetExtendedScanResults_AnalyzerManagerDoesntExist(t *testing.T) { + // Arrange + analyzerManagerExists = false + analyzerManagerExecuter = &analyzerManagerMock{} + + // Act + extendedResults, err := GetExtendedScanResults(fakeBasicXrayResults, fakeBasicDependencyGraph, &fakeServerDetails) + + // Assert + assert.NoError(t, err) + assert.False(t, extendedResults.EntitledForJas) + assert.Equal(t, 1, len(extendedResults.XrayResults)) + assert.Nil(t, extendedResults.ApplicabilityScanResults) +} + +func TestGetExtendedScanResults_ServerNotValid(t *testing.T) { + // Act + extendedResults, err := GetExtendedScanResults(fakeBasicXrayResults, fakeBasicDependencyGraph, nil) + + // Assert + assert.Nil(t, extendedResults) + assert.Error(t, err) + assert.Equal(t, "cant get xray server details", err.Error()) +} diff --git a/xray/audit/jas/secretsscanner.go b/xray/audit/jas/secretsscanner.go new file mode 100644 index 000000000..c249afff6 --- /dev/null +++ b/xray/audit/jas/secretsscanner.go @@ -0,0 +1,180 @@ +package jas + +import ( + "errors" + "fmt" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/owenrumney/go-sarif/v2/sarif" + "gopkg.in/yaml.v2" + "os" + "path/filepath" +) + +const ( + secretsScanCommand = "sec" + secretsScannersNames = "tokens, entropy" + secretsScannerType = "secrets-scan" + secScanFailureMessage = "failed to run secrets scan. Cause: %s" +) + +type SecretScanManager struct { + secretsScannerResults []utils.IacOrSecretResult + configFileName string + resultsFileName string + analyzerManager utils.AnalyzerManagerInterface + serverDetails *config.ServerDetails + projectRootPath string +} + +// The getSecretsScanResults function runs the secrets scan flow, which includes the following steps: +// Creating an SecretScanManager object. +// Running the analyzer manager executable. +// Parsing the analyzer manager results. +// Return values: +// []utils.IacOrSecretResult: a list of the secrets that were found. +// bool: true if the user is entitled to secrets scan, false otherwise. +// error: An error object (if any). +func getSecretsScanResults(serverDetails *config.ServerDetails, analyzerManager utils.AnalyzerManagerInterface) ([]utils.IacOrSecretResult, + bool, error) { + secretScanManager, cleanupFunc, err := newSecretsScanManager(serverDetails, analyzerManager) + if err != nil { + return nil, false, fmt.Errorf(secScanFailureMessage, err.Error()) + } + defer func() { + if cleanupFunc != nil { + err = errors.Join(err, cleanupFunc()) + } + }() + if err = secretScanManager.run(); err != nil { + if utils.IsNotEntitledError(err) || utils.IsUnsupportedCommandError(err) { + return nil, false, nil + } + return nil, true, fmt.Errorf(secScanFailureMessage, err.Error()) + } + return secretScanManager.secretsScannerResults, true, nil +} + +func newSecretsScanManager(serverDetails *config.ServerDetails, analyzerManager utils.AnalyzerManagerInterface) (manager *SecretScanManager, + cleanup func() error, err error) { + tempDir, err := fileutils.CreateTempDir() + if err != nil { + return + } + cleanup = func() error { + return fileutils.RemoveTempDir(tempDir) + } + return &SecretScanManager{ + secretsScannerResults: []utils.IacOrSecretResult{}, + configFileName: filepath.Join(tempDir, "config.yaml"), + resultsFileName: filepath.Join(tempDir, "results.sarif"), + analyzerManager: analyzerManager, + serverDetails: serverDetails, + }, cleanup, nil +} + +func (s *SecretScanManager) run() (err error) { + defer func() { + if deleteJasProcessFiles(s.configFileName, s.resultsFileName) != nil { + deleteFilesError := deleteJasProcessFiles(s.configFileName, s.resultsFileName) + err = errors.Join(err, deleteFilesError) + } + }() + if err = s.createConfigFile(); err != nil { + return + } + if err = s.runAnalyzerManager(); err != nil { + return + } + return s.setScanResults() +} + +type secretsScanConfig struct { + Scans []secretsScanConfiguration `yaml:"scans"` +} + +type secretsScanConfiguration struct { + Roots []string `yaml:"roots"` + Output string `yaml:"output"` + Type string `yaml:"type"` + Scanners string `yaml:"scanners"` + SkippedDirs []string `yaml:"skipped-folders"` +} + +func (s *SecretScanManager) createConfigFile() error { + currentDir, err := coreutils.GetWorkingDirectory() + if err != nil { + return err + } + s.projectRootPath = currentDir + configFileContent := secretsScanConfig{ + Scans: []secretsScanConfiguration{ + { + Roots: []string{currentDir}, + Output: s.resultsFileName, + Type: secretsScannerType, + Scanners: secretsScannersNames, + SkippedDirs: skippedDirs, + }, + }, + } + yamlData, err := yaml.Marshal(&configFileContent) + if errorutils.CheckError(err) != nil { + return err + } + err = os.WriteFile(s.configFileName, yamlData, 0644) + return errorutils.CheckError(err) +} + +func (s *SecretScanManager) runAnalyzerManager() error { + if err := utils.SetAnalyzerManagerEnvVariables(s.serverDetails); err != nil { + return err + } + return s.analyzerManager.Exec(s.configFileName, secretsScanCommand) +} + +func (s *SecretScanManager) setScanResults() error { + report, err := sarif.Open(s.resultsFileName) + if errorutils.CheckError(err) != nil { + return err + } + var secretsResults []*sarif.Result + if len(report.Runs) > 0 { + secretsResults = report.Runs[0].Results + } + + finalSecretsList := []utils.IacOrSecretResult{} + + for _, secret := range secretsResults { + newSecret := utils.IacOrSecretResult{ + Severity: utils.GetResultSeverity(secret), + File: utils.ExtractRelativePath(utils.GetResultFileName(secret), s.projectRootPath), + LineColumn: utils.GetResultLocationInFile(secret), + Text: hideSecret(*secret.Locations[0].PhysicalLocation.Region.Snippet.Text), + Type: *secret.RuleID, + } + finalSecretsList = append(finalSecretsList, newSecret) + } + s.secretsScannerResults = finalSecretsList + return nil +} + +func hideSecret(secret string) string { + if len(secret) <= 3 { + return "***" + } + hiddenSecret := "" + i := 0 + for i < 3 { // Show first 3 digits + hiddenSecret += string(secret[i]) + i++ + } + for i < 15 { + hiddenSecret += "*" + i++ + } + return hiddenSecret +} diff --git a/xray/audit/jas/secretsscanner_test.go b/xray/audit/jas/secretsscanner_test.go new file mode 100644 index 000000000..b7fb7994a --- /dev/null +++ b/xray/audit/jas/secretsscanner_test.go @@ -0,0 +1,128 @@ +package jas + +import ( + "errors" + "fmt" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +func TestNewSecretsScanManager(t *testing.T) { + // Act + secretScanManager, _, err := newSecretsScanManager(&fakeServerDetails, &analyzerManagerMock{}) + + // Assert + assert.NoError(t, err) + assert.NotEmpty(t, secretScanManager) + assert.NotEmpty(t, secretScanManager.configFileName) + assert.NotEmpty(t, secretScanManager.resultsFileName) + assert.Equal(t, &fakeServerDetails, secretScanManager.serverDetails) +} + +func TestSecretsScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { + // Arrange + secretScanManager, _, secretsManagerError := newSecretsScanManager(&fakeServerDetails, &analyzerManagerMock{}) + + // Act + err := secretScanManager.createConfigFile() + + defer func() { + err = os.Remove(secretScanManager.configFileName) + assert.NoError(t, err) + }() + + // Assert + assert.NoError(t, secretsManagerError) + assert.NoError(t, err) + _, fileNotExistError := os.Stat(secretScanManager.configFileName) + assert.NoError(t, fileNotExistError) + fileContent, err := os.ReadFile(secretScanManager.configFileName) + assert.NoError(t, err) + assert.True(t, len(fileContent) > 0) +} + +func TestRunAnalyzerManager_ReturnsGeneralError(t *testing.T) { + defer func() { + os.Clearenv() + analyzerManagerExecutionError = nil + }() + + // Arrange + analyzerManagerExecutionError = errors.New("analyzer manager error") + secretScanManager, _, secretsManagerError := newSecretsScanManager(&fakeServerDetails, &analyzerManagerMock{}) + + // Act + err := secretScanManager.runAnalyzerManager() + + // Assert + assert.NoError(t, secretsManagerError) + assert.Error(t, err) + assert.Equal(t, analyzerManagerExecutionError.Error(), err.Error()) +} + +func TestParseResults_EmptyResults(t *testing.T) { + // Arrange + secretScanManager, _, secretsManagerError := newSecretsScanManager(&fakeServerDetails, &analyzerManagerMock{}) + secretScanManager.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "secrets-scan", "no-secrets.sarif") + + // Act + err := secretScanManager.setScanResults() + + // Assert + assert.NoError(t, secretsManagerError) + assert.NoError(t, err) + assert.Empty(t, secretScanManager.secretsScannerResults) +} + +func TestParseResults_ResultsContainSecrets(t *testing.T) { + // Arrange + secretScanManager, _, secretsManagerError := newSecretsScanManager(&fakeServerDetails, &analyzerManagerMock{}) + secretScanManager.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "secrets-scan", "contain-secrets.sarif") + + // Act + err := secretScanManager.setScanResults() + + // Assert + assert.NoError(t, secretsManagerError) + assert.NoError(t, err) + assert.NotEmpty(t, secretScanManager.secretsScannerResults) + assert.Equal(t, 8, len(secretScanManager.secretsScannerResults)) +} + +func TestGetSecretsScanResults_AnalyzerManagerReturnsError(t *testing.T) { + defer func() { + analyzerManagerExecutionError = nil + }() + + // Arrange + analyzerManagerErrorMessage := "analyzer manager failure message" + analyzerManagerExecutionError = errors.New(analyzerManagerErrorMessage) + + // Act + secretsResults, entitledForSecrets, err := getSecretsScanResults(&fakeServerDetails, &analyzerManagerMock{}) + + // Assert + assert.Error(t, err) + assert.Equal(t, fmt.Sprintf(secScanFailureMessage, analyzerManagerErrorMessage), err.Error()) + assert.Nil(t, secretsResults) + assert.True(t, entitledForSecrets) +} + +func TestHideSecret(t *testing.T) { + tests := []struct { + secret string + expectedOutput string + }{ + {secret: "", expectedOutput: "***"}, + {secret: "12", expectedOutput: "***"}, + {secret: "123", expectedOutput: "***"}, + {secret: "123456789", expectedOutput: "123************"}, + {secret: "3478hfnkjhvd848446gghgfh", expectedOutput: "347************"}, + } + + for _, test := range tests { + assert.Equal(t, test.expectedOutput, hideSecret(test.secret)) + } +} diff --git a/xray/commands/audit/generic/generic.go b/xray/commands/audit/generic/generic.go index 9e79ce237..c8acfc242 100644 --- a/xray/commands/audit/generic/generic.go +++ b/xray/commands/audit/generic/generic.go @@ -1,11 +1,11 @@ package audit import ( + "github.com/jfrog/jfrog-cli-core/v2/xray/audit/jas" "os" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit" commandsutils "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" xrutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -135,10 +135,10 @@ func (auditCmd *GenericAuditCommand) Run() (err error) { if err = errGroup.Wait(); err != nil { return err } - extendedScanResults := &xrutils.ExtendedScanResults{XrayResults: results, ApplicabilityScannerResults: nil, EntitledForJas: false} + extendedScanResults := &xrutils.ExtendedScanResults{XrayResults: results, ApplicabilityScanResults: nil, EntitledForJas: false} // Try to run contextual analysis only if the user is entitled for advance security if entitled { - extendedScanResults, err = audit.GetExtendedScanResults(results, auditParams.FullDependenciesTree(), serverDetails) + extendedScanResults, err = jas.GetExtendedScanResults(results, auditParams.FullDependenciesTree(), serverDetails) if err != nil { return err } diff --git a/xray/commands/testdata/iac-scan/contains-iac-violations.sarif b/xray/commands/testdata/iac-scan/contains-iac-violations.sarif new file mode 100644 index 000000000..f9ee53018 --- /dev/null +++ b/xray/commands/testdata/iac-scan/contains-iac-violations.sarif @@ -0,0 +1,129 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "JFrog Terraform scanner", + "rules": [], + "version": "" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "./tf_scanner", + "scan", + "scan.yaml" + ], + "workingDirectory": { + "uri": "file:///Users/ilya/Downloads/tf-scanner-main/src/dist/tf_scanner" + } + } + ], + "results": [ + { + "message": { + "text": "AWS Load balancer using insecure communications" + }, + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ilya/Downloads/tf-scanner-main/tests/hcl/applicable/req_sw_terraform_aws_alb_https_only.tf" + }, + "region": { + "endColumn": 2, + "endLine": 12, + "snippet": { + "text": "vulnerable_example" + }, + "startColumn": 1, + "startLine": 1 + } + } + } + ], + "ruleId": "aws_alb_https_only" + }, + { + "message": { + "text": "authorization=NONE was detected" + }, + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ilya/Downloads/tf-scanner-main/tests/hcl/applicable/req_sw_terraform_aws_api_gateway_auth.tf" + }, + "region": { + "endColumn": 2, + "endLine": 6, + "snippet": { + "text": "vulnerable_method" + }, + "startColumn": 1, + "startLine": 1 + } + } + } + ], + "ruleId": "aws_api_gateway_auth" + }, + { + "message": { + "text": "cache_data_encrypted=False was detected" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ilya/Downloads/tf-scanner-main/tests/hcl/applicable/req_sw_terraform_aws_api_gateway_encrypt_cache.tf" + }, + "region": { + "endColumn": 2, + "endLine": 8, + "snippet": { + "text": "vulnerable_example" + }, + "startColumn": 1, + "startLine": 1 + } + } + } + ], + "ruleId": "aws_api_gateway_encrypt_cache" + }, + { + "message": { + "text": "security_policy!=TLS_1_2 was detected" + }, + "level": "note", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ilya/Downloads/tf-scanner-main/tests/hcl/applicable/req_sw_terraform_aws_api_gateway_tls_version.tf" + }, + "region": { + "endColumn": 2, + "endLine": 4, + "snippet": { + "text": "vulnerable_example" + }, + "startColumn": 1, + "startLine": 1 + } + } + } + ], + "ruleId": "aws_api_gateway_tls_version" + } + ] + } + ], + "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/iac-scan/no-violations.sarif b/xray/commands/testdata/iac-scan/no-violations.sarif new file mode 100644 index 000000000..0edb478b5 --- /dev/null +++ b/xray/commands/testdata/iac-scan/no-violations.sarif @@ -0,0 +1,30 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "JFrog Terraform scanner", + "rules": [], + "version": "" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "./tf_scanner", + "scan", + "scan.yaml" + ], + "workingDirectory": { + "uri": "file:///Users/ilya/Downloads/tf-scanner-main/src/dist/tf_scanner" + } + } + ], + "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/commands/testdata/secrets-scan/contain-secrets.sarif b/xray/commands/testdata/secrets-scan/contain-secrets.sarif new file mode 100644 index 000000000..a678bab91 --- /dev/null +++ b/xray/commands/testdata/secrets-scan/contain-secrets.sarif @@ -0,0 +1,229 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "JFrog Terraform scanner", + "rules": [ + { + "id": "entropy", + "shortDescription": { + "text": "Scanner for entropy" + } + } + ], + "version": "" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "./secrets_scanner", + "scan", + "sec_config_example.yaml" + ], + "workingDirectory": { + "uri": "file:///Users/ort/Desktop/secrets_scanner" + } + } + ], + "results": [ + { + "message": { + "text": "Hardcoded secrets were found in source files" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ort/Desktop/secrets_scanner/tests/req.nodejs.hardcoded-secrets/applicable_base64.js" + }, + "region": { + "endColumn": 118, + "endLine": 1, + "snippet": { + "text": "2VTHzn1mKZ/n9apD5P6nxsajSQh8QhmyyKvUIRoZWAHCB8lSbBm3YWx5nOdZ1zPEOaA0zIZy1eFgHgfB2HkfAdVrbQj19kagXDVe" + }, + "startColumn": 18, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Hardcoded secrets were found in source files" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ort/Desktop/secrets_scanner/tests/req.nodejs.hardcoded-secrets/applicable_base64.js.approval.json" + }, + "region": { + "endColumn": 195, + "endLine": 1, + "snippet": { + "text": "2VTHzn1mKZ/n9apD5P6nxsajSQh8QhmyyKvUIRoZWAHCB8lSbBm3YWx5nOdZ1zPEOaA0zIZy1eFgHgfB2HkfAdVrbQj19kagXDVe" + }, + "startColumn": 95, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Hardcoded secrets were found in source files" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ort/Desktop/secrets_scanner/tests/req.nodejs.hardcoded-secrets/applicable_hex.js" + }, + "region": { + "endColumn": 138, + "endLine": 1, + "snippet": { + "text": "0159392e31dc912156e1cc6eab32a3d7df7154aecdf2ffe7d66f10da0d5706f7d9ba3183a366389112819b728b20026d04a4f6304da649beefc7fe49" + }, + "startColumn": 18, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Hardcoded secrets were found in source files" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ort/Desktop/secrets_scanner/tests/req.nodejs.hardcoded-secrets/applicable_hex.js.approval.json" + }, + "region": { + "endColumn": 215, + "endLine": 1, + "snippet": { + "text": "0159392e31dc912156e1cc6eab32a3d7df7154aecdf2ffe7d66f10da0d5706f7d9ba3183a366389112819b728b20026d04a4f6304da649beefc7fe49" + }, + "startColumn": 95, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Hardcoded secrets were found in source files" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ort/Desktop/secrets_scanner/tests/req.python.hardcoded-secrets/applicable_base64.py" + }, + "region": { + "endColumn": 112, + "endLine": 1, + "snippet": { + "text": "2VTHzn1mKZ/n9apD5P6nxsajSQh8QhmyyKvUIRoZWAHCB8lSbBm3YWx5nOdZ1zPEOaA0zIZy1eFgHgfB2HkfAdVrbQj19kagXDVe" + }, + "startColumn": 12, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Hardcoded secrets were found in source files" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ort/Desktop/secrets_scanner/tests/req.python.hardcoded-secrets/applicable_base64.py.approval.json" + }, + "region": { + "endColumn": 191, + "endLine": 1, + "snippet": { + "text": "2VTHzn1mKZ/n9apD5P6nxsajSQh8QhmyyKvUIRoZWAHCB8lSbBm3YWx5nOdZ1zPEOaA0zIZy1eFgHgfB2HkfAdVrbQj19kagXDVe" + }, + "startColumn": 91, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Hardcoded secrets were found in source files" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ort/Desktop/secrets_scanner/tests/req.python.hardcoded-secrets/applicable_hex.py" + }, + "region": { + "endColumn": 132, + "endLine": 1, + "snippet": { + "text": "0159392e31dc912156e1cc6eab32a3d7df7154aecdf2ffe7d66f10da0d5706f7d9ba3183a366389112819b728b20026d04a4f6304da649beefc7fe49" + }, + "startColumn": 12, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Hardcoded secrets were found in source files" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/ort/Desktop/secrets_scanner/tests/req.python.hardcoded-secrets/applicable_hex.py.approval.json" + }, + "region": { + "endColumn": 211, + "endLine": 1, + "snippet": { + "text": "0159392e31dc912156e1cc6eab32a3d7df7154aecdf2ffe7d66f10da0d5706f7d9ba3183a366389112819b728b20026d04a4f6304da649beefc7fe49" + }, + "startColumn": 91, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + } + ] + } + ], + "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/secrets-scan/no-secrets.sarif b/xray/commands/testdata/secrets-scan/no-secrets.sarif new file mode 100644 index 000000000..4b3186cd1 --- /dev/null +++ b/xray/commands/testdata/secrets-scan/no-secrets.sarif @@ -0,0 +1,29 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "JFrog Terraform scanner", + "rules": [], + "version": "" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "mac_arm/secrets_scanner/secrets_scanner", + "scan", + "sec_config_example.yaml" + ], + "workingDirectory": { + "uri": "file:///Users/ort/Desktop/am_versions_for_leap" + } + } + ], + "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 cc9acacef..570b1dd3b 100644 --- a/xray/formats/conversion.go +++ b/xray/formats/conversion.go @@ -140,6 +140,32 @@ func ConvertToOperationalRiskViolationScanTableRow(rows []OperationalRiskViolati return } +func ConvertToSecretsTableRow(rows []IacSecretsRow) (tableRows []secretsTableRow) { + for i := range rows { + tableRows = append(tableRows, secretsTableRow{ + severity: rows[i].Severity, + file: rows[i].File, + lineColumn: rows[i].LineColumn, + text: rows[i].Text, + secretType: rows[i].Type, + }) + } + return +} + +func ConvertToIacTableRow(rows []IacSecretsRow) (tableRows []iacTableRow) { + for i := range rows { + tableRows = append(tableRows, iacTableRow{ + severity: rows[i].Severity, + file: rows[i].File, + lineColumn: rows[i].LineColumn, + text: rows[i].Text, + iacType: rows[i].Type, + }) + } + 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 c0c28bb83..54cccdcf8 100644 --- a/xray/formats/simplejsonapi.go +++ b/xray/formats/simplejsonapi.go @@ -12,6 +12,8 @@ type SimpleJsonResults struct { LicensesViolations []LicenseViolationRow `json:"licensesViolations"` Licenses []LicenseRow `json:"licenses"` OperationalRiskViolations []OperationalRiskViolationRow `json:"operationalRiskViolations"` + Secrets []IacSecretsRow `json:"secrets"` + Iacs []IacSecretsRow `json:"iacViolations"` Errors []SimpleJsonError `json:"errors"` } @@ -71,6 +73,15 @@ type OperationalRiskViolationRow struct { LatestVersion string `json:"latestVersion"` } +type IacSecretsRow 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"` +} + type ComponentRow struct { Name string `json:"name"` Version string `json:"version"` diff --git a/xray/formats/table.go b/xray/formats/table.go index 94ec60633..2fc3c389c 100644 --- a/xray/formats/table.go +++ b/xray/formats/table.go @@ -122,3 +122,19 @@ type cveTableRow struct { cvssV2 string `col-name:"CVSS\nv2" extended:"true"` cvssV3 string `col-name:"CVSS\nv3" extended:"true"` } + +type secretsTableRow struct { + severity string `col-name:"Severity"` + file string `col-name:"File"` + lineColumn string `col-name:"Line:Column"` + text string `col-name:"Secret"` + secretType string `col-name:"Type"` +} + +type iacTableRow struct { + severity string `col-name:"Severity"` + file string `col-name:"File"` + lineColumn string `col-name:"Line:Column"` + text string `col-name:"Finding"` + iacType string `col-name:"Scanner"` +} diff --git a/xray/utils/analyzermanager.go b/xray/utils/analyzermanager.go index 5ae8b3ab8..e9181084e 100644 --- a/xray/utils/analyzermanager.go +++ b/xray/utils/analyzermanager.go @@ -3,35 +3,41 @@ package utils import ( "errors" "fmt" - "os" - "os/exec" - "path/filepath" - "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/errorutils" "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" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" ) var ( - analyzerManagerLogFolder = "" - analyzerManagerExecutableName = "analyzerManager" + analyzerManagerLogFolder = "" + levelToSeverity = map[string]string{"error": "High", "warning": "Medium", "info": "Low"} ) const ( - EntitlementsMinVersion = "3.66.5" - ApplicabilityFeatureId = "contextual_analysis" - AnalyzerManagerZipName = "analyzerManager.zip" - analyzerManagerDownloadPath = "xsc-gen-exe-analyzer-manager-local/v1/[RELEASE]" - analyzerManagerDirName = "analyzerManager" - analyzerManagerLogDirName = "analyzerManagerLogs" - jfUserEnvVariable = "JF_USER" - jfPasswordEnvVariable = "JF_PASS" - jfTokenEnvVariable = "JF_TOKEN" - jfPlatformUrlEnvVariable = "JF_PLATFORM_URL" - logDirEnvVariable = "AM_LOG_DIRECTORY" - applicabilityScanCommand = "ca" + EntitlementsMinVersion = "3.66.5" + ApplicabilityFeatureId = "contextual_analysis" + AnalyzerManagerZipName = "analyzerManager.zip" + analyzerManagerDownloadPath = "xsc-gen-exe-analyzer-manager-local/v1/[RELEASE]" + analyzerManagerDirName = "analyzerManager" + analyzerManagerExecutableName = "analyzerManager" + analyzerManagerLogDirName = "analyzerManagerLogs" + jfUserEnvVariable = "JF_USER" + jfPasswordEnvVariable = "JF_PASS" + jfTokenEnvVariable = "JF_TOKEN" + jfPlatformUrlEnvVariable = "JF_PLATFORM_URL" + logDirEnvVariable = "AM_LOG_DIRECTORY" + SeverityDefaultValue = "Medium" + notEntitledExitCode = 31 + unsupportedCommandExitCode = 13 ) const ( @@ -40,10 +46,23 @@ const ( ApplicabilityUndeterminedStringValue = "Undetermined" ) +type IacOrSecretResult struct { + Severity string + File string + LineColumn string + Type string + Text string +} + type ExtendedScanResults struct { - XrayResults []services.ScanResponse - ApplicabilityScannerResults map[string]string - EntitledForJas bool + XrayResults []services.ScanResponse + ApplicabilityScanResults map[string]string + SecretsScanResults []IacOrSecretResult + IacScanResults []IacOrSecretResult + EntitledForJas bool + EligibleForApplicabilityScan bool + EligibleForSecretScan bool + EligibleForIacScan bool } func (e *ExtendedScanResults) getXrayScanResults() []services.ScanResponse { @@ -60,7 +79,7 @@ func (e *ExtendedScanResults) getXrayScanResults() []services.ScanResponse { // - sarif file containing the scan results type AnalyzerManagerInterface interface { ExistLocally() (bool, error) - Exec(string) error + Exec(string, string) error } type AnalyzerManager struct { @@ -76,10 +95,18 @@ func (am *AnalyzerManager) ExistLocally() (bool, error) { return fileutils.IsFileExists(analyzerManagerPath, false) } -func (am *AnalyzerManager) Exec(configFile string) error { - cmd := exec.Command(am.analyzerManagerFullPath, applicabilityScanCommand, configFile) +func (am *AnalyzerManager) Exec(configFile string, scanCommand string) (err error) { + cmd := exec.Command(am.analyzerManagerFullPath, scanCommand, configFile) + defer func() { + if !cmd.ProcessState.Exited() { + if killProcessError := cmd.Process.Kill(); errorutils.CheckError(killProcessError) != nil { + err = errors.Join(err, killProcessError) + } + } + }() cmd.Dir = filepath.Dir(am.analyzerManagerFullPath) - return cmd.Run() + err = cmd.Run() + return errorutils.CheckError(err) } func CreateAnalyzerManagerLogDir() error { @@ -123,18 +150,6 @@ func GetAnalyzerManagerExecutableName() string { return analyzerManager } -func RemoveDuplicateValues(stringSlice []string) []string { - keys := make(map[string]bool) - finalSlice := []string{} - for _, entry := range stringSlice { - if _, value := keys[entry]; !value { - keys[entry] = true - finalSlice = append(finalSlice, entry) - } - } - return finalSlice -} - func SetAnalyzerManagerEnvVariables(serverDetails *config.ServerDetails) error { if serverDetails == nil { return errors.New("cant get xray server details") @@ -156,3 +171,75 @@ func SetAnalyzerManagerEnvVariables(serverDetails *config.ServerDetails) error { } return nil } + +func IsNotEntitledError(err error) bool { + if exitError, ok := err.(*exec.ExitError); ok { + exitCode := exitError.ExitCode() + // User not entitled error + if exitCode == notEntitledExitCode { + log.Debug("got not entitled error from analyzer manager") + return true + } + } + return false +} + +func IsUnsupportedCommandError(err error) bool { + if exitError, ok := err.(*exec.ExitError); ok { + exitCode := exitError.ExitCode() + // Analyzer manager doesn't support the requested scan command + if exitCode == unsupportedCommandExitCode { + log.Debug("got unsupported scan command error from analyzer manager") + return true + } + } + return false +} + +func RemoveDuplicateValues(stringSlice []string) []string { + keys := make(map[string]bool) + finalSlice := []string{} + for _, entry := range stringSlice { + if _, value := keys[entry]; !value { + keys[entry] = true + finalSlice = append(finalSlice, entry) + } + } + 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[*result.Level]; ok { + return severity + } + } + return SeverityDefaultValue +} diff --git a/xray/utils/analyzermanager_test.go b/xray/utils/analyzermanager_test.go index 368afba09..ce90b5210 100644 --- a/xray/utils/analyzermanager_test.go +++ b/xray/utils/analyzermanager_test.go @@ -1,6 +1,7 @@ package utils import ( + "github.com/owenrumney/go-sarif/v2/sarif" "github.com/stretchr/testify/assert" "testing" ) @@ -20,3 +21,115 @@ func TestRemoveDuplicateValues(t *testing.T) { assert.Equal(t, test.expectedResult, RemoveDuplicateValues(test.testedSlice)) } } + +func TestGetResultFileName(t *testing.T) { + fileNameValue := "fileNameValue" + tests := []struct { + result *sarif.Result + expectedOutput string + }{ + {result: &sarif.Result{ + Locations: []*sarif.Location{ + {PhysicalLocation: &sarif.PhysicalLocation{ArtifactLocation: &sarif.ArtifactLocation{URI: nil}}}, + }}, + expectedOutput: ""}, + {result: &sarif.Result{ + Locations: []*sarif.Location{ + {PhysicalLocation: &sarif.PhysicalLocation{ArtifactLocation: &sarif.ArtifactLocation{URI: &fileNameValue}}}, + }}, + expectedOutput: fileNameValue}, + {result: &sarif.Result{}, + expectedOutput: ""}, + } + + for _, test := range tests { + assert.Equal(t, test.expectedOutput, GetResultFileName(test.result)) + } + +} + +func TestGetResultLocationInFile(t *testing.T) { + startLine := 19 + startColumn := 25 + + tests := []struct { + result *sarif.Result + expectedOutput string + }{ + {result: &sarif.Result{Locations: []*sarif.Location{ + {PhysicalLocation: &sarif.PhysicalLocation{Region: &sarif.Region{ + StartLine: &startLine, + StartColumn: &startColumn, + }}}}}, + expectedOutput: "19:25"}, + {result: &sarif.Result{Locations: []*sarif.Location{ + {PhysicalLocation: &sarif.PhysicalLocation{Region: &sarif.Region{ + StartLine: nil, + StartColumn: &startColumn, + }}}}}, + expectedOutput: ""}, + {result: &sarif.Result{Locations: []*sarif.Location{ + {PhysicalLocation: &sarif.PhysicalLocation{Region: &sarif.Region{ + StartLine: &startLine, + StartColumn: nil, + }}}}}, + expectedOutput: ""}, + {result: &sarif.Result{Locations: []*sarif.Location{ + {PhysicalLocation: &sarif.PhysicalLocation{Region: &sarif.Region{ + StartLine: nil, + StartColumn: nil, + }}}}}, + expectedOutput: ""}, + {result: &sarif.Result{}, + expectedOutput: ""}, + } + + for _, test := range tests { + assert.Equal(t, test.expectedOutput, GetResultLocationInFile(test.result)) + } +} + +func TestExtractRelativePath(t *testing.T) { + tests := []struct { + secretPath string + projectPath string + expectedResult string + }{ + {secretPath: "file:///Users/user/Desktop/secrets_scanner/tests/req.nodejs/file.js", + projectPath: "Users/user/Desktop/secrets_scanner/", expectedResult: "/tests/req.nodejs/file.js"}, + {secretPath: "invalidSecretPath", + projectPath: "Users/user/Desktop/secrets_scanner/", expectedResult: "invalidSecretPath"}, + {secretPath: "", + projectPath: "Users/user/Desktop/secrets_scanner/", expectedResult: ""}, + {secretPath: "file:///Users/user/Desktop/secrets_scanner/tests/req.nodejs/file.js", + projectPath: "invalidProjectPath", expectedResult: "/Users/user/Desktop/secrets_scanner/tests/req.nodejs/file.js"}, + } + + for _, test := range tests { + assert.Equal(t, test.expectedResult, ExtractRelativePath(test.secretPath, test.projectPath)) + } +} + +func TestGetResultSeverity(t *testing.T) { + levelValueHigh := "error" + levelValueMedium := "warning" + levelValueLow := "info" + + tests := []struct { + result *sarif.Result + expectedSeverity string + }{ + {result: &sarif.Result{}, + expectedSeverity: "Medium"}, + {result: &sarif.Result{Level: &levelValueHigh}, + expectedSeverity: "High"}, + {result: &sarif.Result{Level: &levelValueMedium}, + expectedSeverity: "Medium"}, + {result: &sarif.Result{Level: &levelValueLow}, + expectedSeverity: "Low"}, + } + + for _, test := range tests { + assert.Equal(t, test.expectedSeverity, GetResultSeverity(test.result)) + } +} diff --git a/xray/utils/resultstable.go b/xray/utils/resultstable.go index 45d74fbe4..02a8644e1 100644 --- a/xray/utils/resultstable.go +++ b/xray/utils/resultstable.go @@ -282,6 +282,80 @@ func PrepareLicenses(licenses []services.License) ([]formats.LicenseRow, error) return licensesRows, nil } +// Prepare secrets for all non-table formats (without style or emoji) +func PrepareSecrets(secrets []IacOrSecretResult) []formats.IacSecretsRow { + return prepareSecrets(secrets, false) +} + +func prepareSecrets(secrets []IacOrSecretResult, isTable bool) []formats.IacSecretsRow { + var secretsRows []formats.IacSecretsRow + for _, secret := range secrets { + currSeverity := GetSeverity(secret.Severity, ApplicableStringValue) + secretsRows = append(secretsRows, + formats.IacSecretsRow{ + Severity: currSeverity.printableTitle(isTable), + SeverityNumValue: currSeverity.numValue, + File: secret.File, + LineColumn: secret.LineColumn, + Text: secret.Text, + Type: secret.Type, + }, + ) + } + + sort.Slice(secretsRows, func(i, j int) bool { + return secretsRows[i].SeverityNumValue > secretsRows[j].SeverityNumValue + }) + + return secretsRows +} + +func PrintSecretsTable(secrets []IacOrSecretResult, entitledForSecretsScan bool) error { + if entitledForSecretsScan { + secretsRows := prepareSecrets(secrets, true) + return coreutils.PrintTable(formats.ConvertToSecretsTableRow(secretsRows), "Secrets", + "✨ No secrets were found ✨", false) + } + return nil +} + +// Prepare iacs for all non-table formats (without style or emoji) +func PrepareIacs(iacs []IacOrSecretResult) []formats.IacSecretsRow { + return prepareIacs(iacs, false) +} + +func prepareIacs(iacs []IacOrSecretResult, isTable bool) []formats.IacSecretsRow { + var iacRows []formats.IacSecretsRow + for _, iac := range iacs { + currSeverity := GetSeverity(iac.Severity, ApplicableStringValue) + iacRows = append(iacRows, + formats.IacSecretsRow{ + Severity: currSeverity.printableTitle(isTable), + SeverityNumValue: currSeverity.numValue, + File: iac.File, + LineColumn: iac.LineColumn, + Text: iac.Text, + Type: iac.Type, + }, + ) + } + + sort.Slice(iacRows, func(i, j int) bool { + return iacRows[i].SeverityNumValue > iacRows[j].SeverityNumValue + }) + + return iacRows +} + +func PrintIacTable(iacs []IacOrSecretResult, entitledForIacScan bool) error { + if entitledForIacScan { + iacRows := prepareIacs(iacs, true) + return coreutils.PrintTable(formats.ConvertToIacTableRow(iacRows), "Iac Violations", + "✨ No Iac violations were found ✨", false) + } + return nil +} + func convertCves(cves []services.Cve) []formats.CveRow { var cveRows []formats.CveRow for _, cveObj := range cves { @@ -730,7 +804,7 @@ func getApplicableCveValue(extendedResults *ExtendedScanResults, xrayCves []form cveExistsInResult := false finalApplicableValue := NotApplicableStringValue for _, cve := range xrayCves { - if currentCveApplicableValue, exists := extendedResults.ApplicabilityScannerResults[cve.Id]; exists { + if currentCveApplicableValue, exists := extendedResults.ApplicabilityScanResults[cve.Id]; exists { cveExistsInResult = true if currentCveApplicableValue == ApplicableStringValue { return currentCveApplicableValue diff --git a/xray/utils/resultstable_test.go b/xray/utils/resultstable_test.go index 116ad66e9..cc16a108e 100644 --- a/xray/utils/resultstable_test.go +++ b/xray/utils/resultstable_test.go @@ -436,28 +436,28 @@ func TestGetApplicableCveValue(t *testing.T) { }{ {scanResults: &ExtendedScanResults{EntitledForJas: false}, expectedResult: ""}, {scanResults: &ExtendedScanResults{ - ApplicabilityScannerResults: map[string]string{"testCve1": ApplicableStringValue, "testCve2": NotApplicableStringValue}, - EntitledForJas: true}, + ApplicabilityScanResults: map[string]string{"testCve1": ApplicableStringValue, "testCve2": NotApplicableStringValue}, + EntitledForJas: true}, cves: nil, expectedResult: ApplicabilityUndeterminedStringValue}, {scanResults: &ExtendedScanResults{ - ApplicabilityScannerResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicableStringValue}, - EntitledForJas: true}, + ApplicabilityScanResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicableStringValue}, + EntitledForJas: true}, cves: []formats.CveRow{{Id: "testCve2"}}, expectedResult: ApplicableStringValue}, {scanResults: &ExtendedScanResults{ - ApplicabilityScannerResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicableStringValue}, - EntitledForJas: true}, + ApplicabilityScanResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicableStringValue}, + EntitledForJas: true}, cves: []formats.CveRow{{Id: "testCve3"}}, expectedResult: ApplicabilityUndeterminedStringValue}, {scanResults: &ExtendedScanResults{ - ApplicabilityScannerResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": NotApplicableStringValue}, - EntitledForJas: true}, + ApplicabilityScanResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": NotApplicableStringValue}, + EntitledForJas: true}, cves: []formats.CveRow{{Id: "testCve1"}, {Id: "testCve2"}}, expectedResult: NotApplicableStringValue}, {scanResults: &ExtendedScanResults{ - ApplicabilityScannerResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicableStringValue}, - EntitledForJas: true}, + ApplicabilityScanResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicableStringValue}, + EntitledForJas: true}, cves: []formats.CveRow{{Id: "testCve1"}, {Id: "testCve2"}}, expectedResult: ApplicableStringValue}, {scanResults: &ExtendedScanResults{ - ApplicabilityScannerResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicabilityUndeterminedStringValue}, - EntitledForJas: true}, + ApplicabilityScanResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicabilityUndeterminedStringValue}, + EntitledForJas: true}, cves: []formats.CveRow{{Id: "testCve1"}, {Id: "testCve2"}}, expectedResult: ApplicabilityUndeterminedStringValue}, } diff --git a/xray/utils/resultwriter.go b/xray/utils/resultwriter.go index dfb14309b..3ed4116eb 100644 --- a/xray/utils/resultwriter.go +++ b/xray/utils/resultwriter.go @@ -97,9 +97,14 @@ func printScanResultsTables(results *ExtendedScanResults, scan, includeVulnerabi return } if includeLicenses { - err = PrintLicensesTable(licenses, printExtended, scan) + if err = PrintLicensesTable(licenses, printExtended, scan); err != nil { + return + } } - return + if err = PrintSecretsTable(results.SecretsScanResults, results.EligibleForSecretScan); err != nil { + return + } + return PrintIacTable(results.IacScanResults, results.EligibleForIacScan) } func printMessages(messages []string) { @@ -149,7 +154,14 @@ func convertScanToSimpleJson(results []services.ScanResponse, extendedResults *E jsonTable.LicensesViolations = licViolationsJsonTable jsonTable.OperationalRiskViolations = opRiskViolationsJsonTable } - + if len(extendedResults.SecretsScanResults) > 0 { + secretsRows := PrepareSecrets(extendedResults.SecretsScanResults) + jsonTable.Secrets = secretsRows + } + if len(extendedResults.IacScanResults) > 0 { + iacRows := PrepareIacs(extendedResults.IacScanResults) + jsonTable.Iacs = iacRows + } if includeLicenses { licJsonTable, err := PrepareLicenses(licenses) if err != nil {