diff --git a/go.mod b/go.mod index 680e0f3a8..0bd474cf8 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/jfrog/build-info-go v1.9.35 github.com/jfrog/froggit-go v1.16.1 github.com/jfrog/gofrog v1.7.5 - github.com/jfrog/jfrog-cli-core/v2 v2.55.6 - github.com/jfrog/jfrog-cli-security v1.7.2 + github.com/jfrog/jfrog-cli-core/v2 v2.55.7 + github.com/jfrog/jfrog-cli-security v1.8.0 github.com/jfrog/jfrog-client-go v1.46.1 github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/owenrumney/go-sarif/v2 v2.3.1 @@ -119,8 +119,7 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect ) -// attiasas:dockerscan_sarif_imp -replace github.com/jfrog/jfrog-cli-security => github.com/attiasas/jfrog-cli-security v0.0.0-20240904115644-bb15ff25795e +// replace github.com/jfrog/jfrog-cli-security => github.com/jfrog/jfrog-cli-security dev // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 dev diff --git a/go.sum b/go.sum index deba35f9f..ba1dca8ef 100644 --- a/go.sum +++ b/go.sum @@ -633,8 +633,6 @@ github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/attiasas/jfrog-cli-security v0.0.0-20240904115644-bb15ff25795e h1:6gfhwBjKr/MghP7ZwPFR1pvqg7mb//PdE5mCMk3vu/M= -github.com/attiasas/jfrog-cli-security v0.0.0-20240904115644-bb15ff25795e/go.mod h1:4eztJ+gBb7Xtq/TtnOvIodBOMZutPIAZOuLxqHWXrOo= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -901,8 +899,10 @@ github.com/jfrog/gofrog v1.7.5 h1:dFgtEDefJdlq9cqTRoe09RLxS5Bxbe1Ev5+E6SmZHcg= github.com/jfrog/gofrog v1.7.5/go.mod h1:jyGiCgiqSSR7k86hcUSu67XVvmvkkgWTmPsH25wI298= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= -github.com/jfrog/jfrog-cli-core/v2 v2.55.6 h1:3tQuEdYgS2q7fkrrSG66OnO0S998FXGaY9BVsxSLst4= -github.com/jfrog/jfrog-cli-core/v2 v2.55.6/go.mod h1:DPO5BfWAeOByahFMMy+PcjmbPlcyoRy7Bf2C5sGKVi0= +github.com/jfrog/jfrog-cli-core/v2 v2.55.7 h1:V4dO2FMNIH49lov3dMj3jYRg8KBTG7hyhHI8ftYByf8= +github.com/jfrog/jfrog-cli-core/v2 v2.55.7/go.mod h1:DPO5BfWAeOByahFMMy+PcjmbPlcyoRy7Bf2C5sGKVi0= +github.com/jfrog/jfrog-cli-security v1.8.0 h1:jp/AVaQcItUNXRCud5PMyl8VVjPuzfrNHJWQvWAMnms= +github.com/jfrog/jfrog-cli-security v1.8.0/go.mod h1:DjufYZpsTwILOFJlx7tR/y63oLBRmtPtFIz1WgiP/X4= github.com/jfrog/jfrog-client-go v1.46.1 h1:ExqOF8ClOG9LO3vbm6jTIwQHHhprbu8lxB2RrM6mMI0= github.com/jfrog/jfrog-client-go v1.46.1/go.mod h1:UCu2JNBfMp9rypEmCL84DCooG79xWIHVadZQR3Ab+BQ= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= diff --git a/scanpullrequest/scanpullrequest.go b/scanpullrequest/scanpullrequest.go index 905f464d5..7d41082e0 100644 --- a/scanpullrequest/scanpullrequest.go +++ b/scanpullrequest/scanpullrequest.go @@ -132,7 +132,8 @@ func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient, scanDetails := utils.NewScanDetails(client, &repoConfig.Server, &repoConfig.Git). SetXrayGraphScanParams(repoConfig.Watches, repoConfig.JFrogProjectKey, len(repoConfig.AllowedLicenses) > 0). SetFixableOnly(repoConfig.FixableOnly). - SetFailOnInstallationErrors(*repoConfig.FailOnSecurityIssues) + SetFailOnInstallationErrors(*repoConfig.FailOnSecurityIssues). + SetConfigProfile(repoConfig.ConfigProfile) if scanDetails, err = scanDetails.SetMinSeverity(repoConfig.MinSeverity); err != nil { return } diff --git a/scanpullrequest/scanpullrequest_test.go b/scanpullrequest/scanpullrequest_test.go index 74d323a25..0bed85015 100644 --- a/scanpullrequest/scanpullrequest_test.go +++ b/scanpullrequest/scanpullrequest_test.go @@ -150,7 +150,7 @@ func TestCreateVulnerabilitiesRowsCaseNoPrevViolations(t *testing.T) { IssueId: "XRAY-1", Summary: "summary-1", ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 15}, ImpactedDependencyName: "component-A", }, }, @@ -158,7 +158,7 @@ func TestCreateVulnerabilitiesRowsCaseNoPrevViolations(t *testing.T) { IssueId: "XRAY-2", Summary: "summary-2", ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Low"}, + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 9}, ImpactedDependencyName: "component-C", }, }, @@ -342,7 +342,7 @@ func TestGetNewVulnerabilitiesCaseNoPrevVulnerabilities(t *testing.T) { Summary: "summary-2", IssueId: "XRAY-2", ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Low"}, + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 9}, ImpactedDependencyName: "component-B", }, JfrogResearchInformation: &formats.JfrogResearchInformation{Details: "description-2"}, @@ -351,7 +351,7 @@ func TestGetNewVulnerabilitiesCaseNoPrevVulnerabilities(t *testing.T) { Summary: "summary-1", IssueId: "XRAY-1", ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 15}, ImpactedDependencyName: "component-A", }, JfrogResearchInformation: &formats.JfrogResearchInformation{Details: "description-1"}, diff --git a/testdata/configprofile/configProfileExample.json b/testdata/configprofile/configProfileExample.json new file mode 100644 index 000000000..d0cac239d --- /dev/null +++ b/testdata/configprofile/configProfileExample.json @@ -0,0 +1,49 @@ +{ + "profile_name": "default-profile", + "frogbot_config": { + "email_author": "my-user@jfrog.com", + "aggregate_fixes": true, + "avoid_previous_pr_comments_deletion": true, + "branch_name_template": "frogbot-${IMPACTED_PACKAGE}-${BRANCH_NAME_HASH}", + "pr_title_template": "[🐸 Frogbot] Upgrade {IMPACTED_PACKAGE} to {FIX_VERSION}", + "pr_comment_title": "Frogbot notes:", + "commit_message_template": "Upgrade {IMPACTED_PACKAGE} to {FIX_VERSION}", + "show_secrets_as_pr_comment": false + }, + "modules": [ + { + "module_name": "default-module", + "path_from_root": ".", + "releases_repo": "nuget-remote", + "analyzer_manager_version": "1.8.1", + "additional_paths_for_module": ["lib1", "utils/lib2"], + "exclude_paths": ["**/.git/**", "**/*test*/**", "**/*venv*/**", "**/*node_modules*/**", "**/target/**"], + "scan_config": { + "scan_timeout": 600, + "exclude_pattern": "*.md", + "enable_sca_scan": true, + "enable_contextual_analysis_scan": true, + "sast_scanner_config": { + "enable_sast_scan": true + }, + "secrets_scanner_config": { + "enable_secrets_scan": true + }, + "iac_scanner_config": { + "enable_iac_scan": true + }, + "applications_scanner_config": { + "enable_applications_scan": true + }, + "services_scanner_config": { + "enable_services_scan": true + } + }, + "protected_branches": ["main", "master"], + "include_exclude_mode": 0, + "include_exclude_pattern": "*test*", + "report_analytics": true + } + ], + "is_default": true +} \ No newline at end of file diff --git a/utils/consts.go b/utils/consts.go index b9a705234..529a2c62e 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -31,6 +31,7 @@ const ( jfrogReleasesRepoEnv = "JF_RELEASES_REPO" JFrogPasswordEnv = "JF_PASSWORD" JFrogTokenEnv = "JF_ACCESS_TOKEN" + JfrogConfigProfileEnv = "JF_CONFIG_PROFILE" // Git environment variables GitProvider = "JF_GIT_PROVIDER" diff --git a/utils/params.go b/utils/params.go index 6e6f27b07..02fa1190c 100644 --- a/utils/params.go +++ b/utils/params.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "github.com/jfrog/jfrog-cli-security/utils/xsc" + "github.com/jfrog/jfrog-client-go/xsc/services" "net/http" "net/url" "os" @@ -136,6 +138,7 @@ type Scan struct { AllowedLicenses []string `yaml:"allowedLicenses,omitempty"` Projects []Project `yaml:"projects,omitempty"` EmailDetails `yaml:",inline"` + ConfigProfile *services.ConfigProfile } type EmailDetails struct { @@ -354,6 +357,12 @@ func GetFrogbotDetails(commandName string) (frogbotDetails *FrogbotDetails, err if err != nil { return } + + configProfile, err := getConfigProfileIfExistsAndValid(jfrogServer) + if err != nil { + return + } + gitParamsFromEnv, err := extractGitParamsFromEnvs(commandName) if err != nil { return @@ -381,6 +390,11 @@ func GetFrogbotDetails(commandName string) (frogbotDetails *FrogbotDetails, err return } + // We apply the configProfile to all received repositories. This loop must be deleted when we will no longer accept multiple repositories in a single scan + for i := range configAggregator { + configAggregator[i].Scan.ConfigProfile = configProfile + } + frogbotDetails = &FrogbotDetails{Repositories: configAggregator, GitClient: client, ServerDetails: jfrogServer, ReleasesRepo: os.Getenv(jfrogReleasesRepoEnv)} return } @@ -706,3 +720,29 @@ func readConfigFromTarget(client vcsclient.VcsClient, gitParamsFromEnv *Git) (co } return } + +// This function fetches a config profile if JF_CONFIG_PROFILE is provided. +// If so - it verifies there is only a single module with a '.' path from root. If these conditions doesn't hold we return an error. +func getConfigProfileIfExistsAndValid(jfrogServer *coreconfig.ServerDetails) (configProfile *services.ConfigProfile, err error) { + profileName := getTrimmedEnv(JfrogConfigProfileEnv) + if profileName == "" { + log.Debug(fmt.Sprintf("No %s environment variable was provided. All configurations will be induced from Env vars and files", JfrogConfigProfileEnv)) + return + } + + if configProfile, err = xsc.GetConfigProfile(jfrogServer, profileName); err != nil { + return + } + + // Currently, only a single Module that represents the entire project is supported + if len(configProfile.Modules) != 1 { + err = fmt.Errorf("more than one module was found '%s' profile. Frogbot currently supports only one module per config profile", configProfile.ProfileName) + return + } + if configProfile.Modules[0].PathFromRoot != "." { + err = fmt.Errorf("module '%s' in profile '%s' contains the following path from root: '%s'. Frogbot currently supports only a single module with a '.' path from root", configProfile.Modules[0].ModuleName, profileName, configProfile.Modules[0].PathFromRoot) + return + } + log.Info(fmt.Sprintf("Using Config profile '%s'. jfrog-apps-config will be ignored if exists", profileName)) + return +} diff --git a/utils/params_test.go b/utils/params_test.go index 1f4b571ff..e3db1ef0a 100644 --- a/utils/params_test.go +++ b/utils/params_test.go @@ -1,8 +1,11 @@ package utils import ( + "encoding/json" "errors" "fmt" + "github.com/jfrog/jfrog-client-go/utils/tests" + "github.com/jfrog/jfrog-client-go/xsc/services" "os" "path/filepath" "testing" @@ -17,6 +20,7 @@ import ( var ( configParamsTestFile = filepath.Join("..", "testdata", "config", "frogbot-config-test-params.yml") configEmptyScanParamsTestFile = filepath.Join("..", "testdata", "config", "frogbot-config-empty-scan.yml") + configProfileFile = filepath.Join("..", "testdata", "configprofile", "configProfileExample.json") ) func TestExtractParamsFromEnvError(t *testing.T) { @@ -678,3 +682,47 @@ func TestSetEmailDetails(t *testing.T) { }) } } + +func TestGetConfigProfileIfExistsAndValid(t *testing.T) { + testcases := []struct { + profileName string + failureExpected bool + }{ + { + profileName: ValidConfigProfile, + failureExpected: false, + }, + { + profileName: InvalidPathConfigProfile, + failureExpected: true, + }, + { + profileName: InvalidModulesConfigProfile, + failureExpected: true, + }, + } + + for _, testcase := range testcases { + t.Run(testcase.profileName, func(t *testing.T) { + envCallbackFunc := tests.SetEnvWithCallbackAndAssert(t, JfrogConfigProfileEnv, testcase.profileName) + defer envCallbackFunc() + + mockServer, serverDetails := CreateXscMockServerForConfigProfile(t) + defer mockServer.Close() + + configProfile, err := getConfigProfileIfExistsAndValid(serverDetails) + if testcase.failureExpected { + assert.Error(t, err) + } else { + assert.NoError(t, err) + var configProfileContentForComparison []byte + configProfileContentForComparison, err = os.ReadFile(configProfileFile) + assert.NoError(t, err) + var configProfileFromFile services.ConfigProfile + err = json.Unmarshal(configProfileContentForComparison, &configProfileFromFile) + assert.NoError(t, err) + assert.Equal(t, configProfileFromFile, *configProfile) + } + }) + } +} diff --git a/utils/scandetails.go b/utils/scandetails.go index a029cf4c0..d1a1f11f2 100644 --- a/utils/scandetails.go +++ b/utils/scandetails.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + clientservices "github.com/jfrog/jfrog-client-go/xsc/services" "os" "path/filepath" @@ -28,6 +29,7 @@ type ScanDetails struct { fixableOnly bool minSeverityFilter severityutils.Severity baseBranch string + configProfile *clientservices.ConfigProfile } func NewScanDetails(client vcsclient.VcsClient, server *config.ServerDetails, git *Git) *ScanDetails { @@ -71,6 +73,11 @@ func (sc *ScanDetails) SetBaseBranch(branch string) *ScanDetails { return sc } +func (sc *ScanDetails) SetConfigProfile(configProfile *clientservices.ConfigProfile) *ScanDetails { + sc.configProfile = configProfile + return sc +} + func (sc *ScanDetails) Client() vcsclient.VcsClient { return sc.client } @@ -153,7 +160,8 @@ func (sc *ScanDetails) RunInstallAndAudit(workDirs ...string) (auditResults *xra SetMinSeverityFilter(sc.MinSeverityFilter()). SetFixableOnly(sc.FixableOnly()). SetGraphBasicParams(auditBasicParams). - SetCommonGraphScanParams(sc.CreateCommonGraphScanParams()) + SetCommonGraphScanParams(sc.CreateCommonGraphScanParams()). + SetConfigProfile(sc.configProfile) auditParams.SetExclusions(sc.PathExclusions).SetIsRecursiveScan(sc.IsRecursiveScan) auditResults, err = audit.RunAudit(auditParams) diff --git a/utils/testsutils.go b/utils/testsutils.go index 8cde7239e..566cc4f13 100644 --- a/utils/testsutils.go +++ b/utils/testsutils.go @@ -1,7 +1,11 @@ package utils import ( + "encoding/json" "fmt" + "github.com/jfrog/jfrog-client-go/xsc/services" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -18,6 +22,12 @@ import ( "github.com/stretchr/testify/assert" ) +const ( + ValidConfigProfile = "default-profile" + InvalidPathConfigProfile = "invalid-path-from-root-profile" + InvalidModulesConfigProfile = "invalid-modules-profile" +) + // Receive an environment variables key-values map, set and assert the environment variables. // Return a callback that sets the previous values. func SetEnvAndAssert(t *testing.T, env map[string]string) { @@ -41,6 +51,7 @@ func unsetEnvAndAssert(t *testing.T, key string) { assert.NoError(t, os.Unsetenv(key)) } +// This function takes a map of environment variables and sets them, and returns a callback to UNSET them all func SetEnvsAndAssertWithCallback(t *testing.T, envs map[string]string) func() { for key, val := range envs { setEnvAndAssert(t, key, val) @@ -142,3 +153,66 @@ func CreateTempJfrogHomeWithCallback(t *testing.T) (string, func()) { assert.NoError(t, fileutils.RemoveTempDir(newJfrogHomeDir)) } } + +func CreateXscMockServerForConfigProfile(t *testing.T) (mockServer *httptest.Server, serverDetails *config.ServerDetails) { + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + secondModule := services.Module{ + ModuleId: 999, + ModuleName: "second-module", + PathFromRoot: ".", + ScanConfig: services.ScanConfig{ + ScanTimeout: 0, + ExcludePattern: "", + EnableScaScan: false, + EnableContextualAnalysisScan: false, + }, + } + + switch { + case strings.HasPrefix(r.RequestURI, "/xsc/api/v1/profile/"): + assert.Equal(t, http.MethodGet, r.Method) + if r.RequestURI == "/xsc/api/v1/profile/"+ValidConfigProfile { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusBadRequest) + } + + content, err := os.ReadFile("../testdata/configprofile/configProfileExample.json") + assert.NoError(t, err) + + if r.RequestURI == "/xsc/api/v1/profile/"+InvalidModulesConfigProfile { + // Adding a second module to make the profile invalid, as we currently support ONLY profile with a single module + var profile services.ConfigProfile + err = json.Unmarshal(content, &profile) + assert.NoError(t, err) + profile.Modules = append(profile.Modules, secondModule) + content, err = json.Marshal(profile) + assert.NoError(t, err) + } + + if r.RequestURI == "/xsc/api/v1/profile/"+InvalidPathConfigProfile { + // Changing 'path_from_root' to a path different from '.' to make the module invalid, as we currently support ONLY a single module with '.' path + updatedContent := string(content) + updatedContent = strings.Replace(updatedContent, `"path_from_root": "."`, `"path_from_root": "backend"`, 1) + content = []byte(updatedContent) + } + + _, err = w.Write(content) + assert.NoError(t, err) + + case r.RequestURI == "/xsc/api/v1/system/version": + _, err := w.Write([]byte(fmt.Sprintf(`{"xsc_version": "%s"}`, services.ConfigProfileMinXscVersion))) + assert.NoError(t, err) + default: + assert.Fail(t, "received an unexpected request") + } + })) + + url := mockServer.URL + serverDetails = &config.ServerDetails{ + Url: url + "/", + XrayUrl: url + "/xray/", + XscUrl: url + "/xsc/", + } + return +}