From c540ed956e1b0797e57b72f7438a383505653125 Mon Sep 17 00:00:00 2001 From: Omer Zidkoni <50792403+omerzi@users.noreply.github.com> Date: Wed, 12 Jul 2023 20:07:31 +0300 Subject: [PATCH 1/6] Align dev branch with master (#861) --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 7ab5f356e..f88563ffd 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/jedib0t/go-pretty/v6 v6.4.6 github.com/jfrog/build-info-go v1.9.6 github.com/jfrog/gofrog v1.3.0 - github.com/jfrog/jfrog-client-go v1.31.0 + github.com/jfrog/jfrog-client-go v1.31.1 github.com/magiconair/properties v1.8.7 github.com/manifoldco/promptui v0.9.0 github.com/owenrumney/go-sarif/v2 v2.1.3 @@ -94,7 +94,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230705083849-6fd087a5e228 +// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230705083849-6fd087a5e228 // replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20230518114837-fe6a826d5001 diff --git a/go.sum b/go.sum index 07dfff302..6c6d6437a 100644 --- a/go.sum +++ b/go.sum @@ -198,8 +198,8 @@ github.com/jfrog/build-info-go v1.9.6 h1:lCJ2j5uXAlJsSwDe5J8WD7Co1f/hUlZvMfwfb5A github.com/jfrog/build-info-go v1.9.6/go.mod h1:GbuFS+viHCKZYx9nWHYu7ab1DgQkFdtVN3BJPUNb2D4= github.com/jfrog/gofrog v1.3.0 h1:o4zgsBZE4QyDbz2M7D4K6fXPTBJht+8lE87mS9bw7Gk= github.com/jfrog/gofrog v1.3.0/go.mod h1:IFMc+V/yf7rA5WZ74CSbXe+Lgf0iApEQLxRZVzKRUR0= -github.com/jfrog/jfrog-client-go v1.28.1-0.20230705083849-6fd087a5e228 h1:bkOkjb6sVqo6Jgw9eYSH58jIFPOJvFvwt+jIXvef7QM= -github.com/jfrog/jfrog-client-go v1.28.1-0.20230705083849-6fd087a5e228/go.mod h1:qEJxoe68sUtqHJ1YhXv/7pKYP/9p1D5tJrruzJKYeoI= +github.com/jfrog/jfrog-client-go v1.31.1 h1:lmunA5ZpRsrWTXgEGvnvVPIfwEqB3gn6+eVNpV2VBzU= +github.com/jfrog/jfrog-client-go v1.31.1/go.mod h1:qEJxoe68sUtqHJ1YhXv/7pKYP/9p1D5tJrruzJKYeoI= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= From a726800a20a27474219e0848834a72c4dfa9fb67 Mon Sep 17 00:00:00 2001 From: Omer Zidkoni <50792403+omerzi@users.noreply.github.com> Date: Mon, 17 Jul 2023 08:40:02 +0300 Subject: [PATCH 2/6] Run the scanners independently from the SCA scanner (#864) --- xray/audit/jas/applicabilitymanager.go | 3 ++- xray/audit/jas/iacscanner.go | 5 +++++ xray/audit/jas/secretsscanner.go | 5 +++++ xray/commands/audit/generic/auditmanager.go | 12 ++++++------ xray/commands/curation/audit.go | 7 ++----- xray/commands/utils/utils.go | 7 ++++--- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/xray/audit/jas/applicabilitymanager.go b/xray/audit/jas/applicabilitymanager.go index 818a66481..c7f541972 100644 --- a/xray/audit/jas/applicabilitymanager.go +++ b/xray/audit/jas/applicabilitymanager.go @@ -51,7 +51,7 @@ func getApplicabilityScanResults(results []services.ScanResponse, dependencyTree } }() if !applicabilityScanManager.eligibleForApplicabilityScan() { - log.Debug("The conditions for running the applicability scan are not met. Skipping the execution of the Analyzer Manager") + log.Debug("The conditions for running the applicability scan are not met. Skipping...") return nil, false, nil } if err = applicabilityScanManager.run(); err != nil { @@ -191,6 +191,7 @@ func (a *ApplicabilityScanManager) run() (err error) { if !a.directDependenciesExist() { return nil } + log.Info("Running applicability scanning for the identified vulnerable dependencies...") if err = a.createConfigFile(); err != nil { return } diff --git a/xray/audit/jas/iacscanner.go b/xray/audit/jas/iacscanner.go index 291fa1452..1fd087a55 100644 --- a/xray/audit/jas/iacscanner.go +++ b/xray/audit/jas/iacscanner.go @@ -8,6 +8,7 @@ import ( "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/owenrumney/go-sarif/v2/sarif" "gopkg.in/yaml.v2" "os" @@ -48,12 +49,16 @@ func getIacScanResults(serverDetails *config.ServerDetails, analyzerManager util err = errors.Join(err, cleanupFunc()) } }() + log.Info("Running IaC scanning...") 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()) } + if len(iacScanManager.iacScannerResults) > 0 { + log.Info("Found", len(iacScanManager.iacScannerResults), "IaC vulnerabilities") + } return iacScanManager.iacScannerResults, true, nil } diff --git a/xray/audit/jas/secretsscanner.go b/xray/audit/jas/secretsscanner.go index c249afff6..9d1a26921 100644 --- a/xray/audit/jas/secretsscanner.go +++ b/xray/audit/jas/secretsscanner.go @@ -8,6 +8,7 @@ import ( "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/owenrumney/go-sarif/v2/sarif" "gopkg.in/yaml.v2" "os" @@ -49,12 +50,16 @@ func getSecretsScanResults(serverDetails *config.ServerDetails, analyzerManager err = errors.Join(err, cleanupFunc()) } }() + log.Info("Running secrets scanning...") 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()) } + if len(secretScanManager.secretsScannerResults) > 0 { + log.Info(len(secretScanManager.secretsScannerResults), "secrets were found") + } return secretScanManager.secretsScannerResults, true, nil } diff --git a/xray/commands/audit/generic/auditmanager.go b/xray/commands/audit/generic/auditmanager.go index d506394f8..2fe44e1d5 100644 --- a/xray/commands/audit/generic/auditmanager.go +++ b/xray/commands/audit/generic/auditmanager.go @@ -135,7 +135,7 @@ func RunAudit(auditParams *Params) (results *Results, err error) { } extendedScanResults := &clientUtils.ExtendedScanResults{XrayResults: scanResults} - // Try to run contextual analysis only if the user is entitled for advance security + // Run scanners only if the user is entitled for Advanced Security if isEntitled { extendedScanResults, err = jas.GetExtendedScanResults(scanResults, auditParams.FullDependenciesTree(), serverDetails) if err != nil { @@ -170,9 +170,8 @@ func genericAudit(params *Params) (results []services.ScanResponse, isMultipleRo return } log.Info("JFrog Xray version is:", params.xrayVersion) - + log.Info("Scanning for vulnerable dependencies...") if len(params.workingDirs) == 0 { - log.Info("Auditing project...") return doAudit(params) } @@ -194,7 +193,7 @@ func auditMultipleWorkingDirs(params *Params) (results []services.ScanResponse, errorList.WriteString(fmt.Sprintf("the audit command couldn't find the following path: %s\n%s\n", wd, e.Error())) continue } - log.Info("Auditing project:", absWd, "...") + log.Info("Scanning directory:", absWd, "...") e = os.Chdir(absWd) if e != nil { errorList.WriteString(fmt.Sprintf("the audit command couldn't change the current working directory to the following path: %s\n%s\n", absWd, e.Error())) @@ -224,8 +223,9 @@ func doAudit(params *Params) (results []services.ScanResponse, isMultipleRoot bo // Otherwise, run audit for requested technologies only. technologies := params.Technologies() if len(technologies) == 0 { - technologies, err = commandsutils.DetectedTechnologies() - if err != nil { + technologies = commandsutils.DetectedTechnologies() + if len(technologies) == 0 { + log.Info("Skipping vulnerable dependencies scanning...") return } } diff --git a/xray/commands/curation/audit.go b/xray/commands/curation/audit.go index 6703f7e1c..ce59134a2 100644 --- a/xray/commands/curation/audit.go +++ b/xray/commands/curation/audit.go @@ -178,16 +178,13 @@ func (ca *CurationAuditCommand) Run() (err error) { } func (ca *CurationAuditCommand) doCurateAudit(results map[string][]*PackageStatus) error { - techs, err := cmdUtils.DetectedTechnologies() - if err != nil { - return err - } + techs := cmdUtils.DetectedTechnologies() for _, tech := range techs { if _, ok := supportedTech[coreutils.Technology(tech)]; !ok { log.Info(fmt.Sprintf(errorTemplateUnsupportedTech, tech)) continue } - if err = ca.auditTree(coreutils.Technology(tech), results); err != nil { + if err := ca.auditTree(coreutils.Technology(tech), results); err != nil { return err } } diff --git a/xray/commands/utils/utils.go b/xray/commands/utils/utils.go index fdaed4626..678588b7e 100644 --- a/xray/commands/utils/utils.go +++ b/xray/commands/utils/utils.go @@ -185,7 +185,7 @@ func CreateXrayServiceManagerAndGetVersion(serviceDetails *config.ServerDetails) return xrayManager, xrayVersion, nil } -func DetectedTechnologies() (technologies []string, err error) { +func DetectedTechnologies() (technologies []string) { wd, err := os.Getwd() if errorutils.CheckError(err) != nil { return @@ -196,10 +196,11 @@ func DetectedTechnologies() (technologies []string, err error) { } detectedTechnologiesString := coreutils.DetectedTechnologiesToString(detectedTechnologies) if detectedTechnologiesString == "" { - return nil, errorutils.CheckErrorf("could not determine the package manager / build tool used by this project.") + log.Info("Couldn't determine a package manager or build tool used by this project in the current path:", wd) + return } log.Info("Detected: " + detectedTechnologiesString) - return coreutils.DetectedTechnologiesToSlice(detectedTechnologies), nil + return coreutils.DetectedTechnologiesToSlice(detectedTechnologies) } func DetectNumOfThreads(threadsCount int) (int, error) { From cd976eb1e5c3ce0d2434ba4e0191d727b3d7b369 Mon Sep 17 00:00:00 2001 From: Sara Omari <114062096+sarao1310@users.noreply.github.com> Date: Mon, 17 Jul 2023 12:29:20 +0300 Subject: [PATCH 3/6] New `exclusions` option for go-publish (#847) * add --exclusions flag to go-publish --- .../commands/buildinfo/adddependencies.go | 2 +- artifactory/commands/golang/archive.go | 302 +++--------------- artifactory/commands/golang/archive_test.go | 49 ++- artifactory/commands/golang/gopublish.go | 12 +- artifactory/commands/golang/publish.go | 9 +- .../commands/golang/testdata/dir2/dir2.text | 1 + .../commands/golang/testdata/dir3/c.txt | 1 + .../golang/testdata/dir3/dir4/dir4.txt | 1 + go.mod | 2 +- go.sum | 5 +- xray/commands/scan/scan.go | 2 +- 11 files changed, 111 insertions(+), 275 deletions(-) create mode 100644 artifactory/commands/golang/testdata/dir2/dir2.text create mode 100644 artifactory/commands/golang/testdata/dir3/c.txt create mode 100644 artifactory/commands/golang/testdata/dir3/dir4/dir4.txt diff --git a/artifactory/commands/buildinfo/adddependencies.go b/artifactory/commands/buildinfo/adddependencies.go index 01593df63..1e0e77196 100644 --- a/artifactory/commands/buildinfo/adddependencies.go +++ b/artifactory/commands/buildinfo/adddependencies.go @@ -256,7 +256,7 @@ func getLocalDependencies(addDepsParams *specutils.CommonParams) ([]string, erro func collectPatternMatchingFiles(addDepsParams *specutils.CommonParams, rootPath string) ([]string, error) { addDepsParams.SetPattern(clientutils.ConvertLocalPatternToRegexp(addDepsParams.Pattern, addDepsParams.GetPatternType())) - excludePathPattern := fspatterns.PrepareExcludePathPattern(addDepsParams) + excludePathPattern := fspatterns.PrepareExcludePathPattern(addDepsParams.Exclusions, addDepsParams.GetPatternType(), addDepsParams.IsRecursive()) patternRegex, err := regxp.Compile(addDepsParams.Pattern) if errorutils.CheckError(err) != nil { return nil, err diff --git a/artifactory/commands/golang/archive.go b/artifactory/commands/golang/archive.go index 674fa3c7e..b81ef3313 100644 --- a/artifactory/commands/golang/archive.go +++ b/artifactory/commands/golang/archive.go @@ -30,19 +30,15 @@ package golang // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import ( - "archive/zip" - "bytes" - "fmt" + "github.com/jfrog/jfrog-client-go/artifactory/services/fspatterns" + "github.com/jfrog/jfrog-client-go/utils" + "golang.org/x/mod/module" + gozip "golang.org/x/mod/zip" "io" "os" - "path" "path/filepath" + "regexp" "strings" - "unicode" - "unicode/utf8" - - "github.com/jfrog/jfrog-client-go/utils/errorutils" - "golang.org/x/mod/module" ) // Package zip provides functions for creating and extracting module zip files. @@ -77,27 +73,16 @@ import ( // Note that this package does not provide hashing functionality. See // golang.org/x/mod/sumdb/dirhash. -const ( - // MaxZipFile is the maximum size in bytes of a module zip file. The - // go command will report an error if either the zip file or its extracted - // content is larger than this. - MaxZipFile = 500 << 20 - - // MaxGoMod is the maximum size in bytes of a go.mod file within a - // module zip file. - MaxGoMod = 16 << 20 - - // MaxLICENSE is the maximum size in bytes of a LICENSE file within a - // module zip file. - MaxLICENSE = 16 << 20 -) - // Archive project files according to the go project standard -func archiveProject(writer io.Writer, dir, mod, version string) error { +func archiveProject(writer io.Writer, dir, mod, version string, excludedPatterns []string) error { m := module.Version{Version: version, Path: mod} - var files []File - - err := filepath.Walk(dir, func(filePath string, info os.FileInfo, err error) error { + excludedPatterns, err := getAbsolutePaths(excludedPatterns) + if err != nil { + return err + } + excludePatternsStr := fspatterns.PrepareExcludePathPattern(excludedPatterns, utils.GetPatternType(utils.PatternTypes{RegExp: false, Ant: false}), true) + var files []gozip.File + err = filepath.Walk(dir, func(filePath string, info os.FileInfo, err error) error { if err != nil { return err } @@ -136,11 +121,17 @@ func archiveProject(writer io.Writer, dir, mod, version string) error { } if info.Mode().IsRegular() { if !isVendoredPackage(slashPath) { - files = append(files, dirFile{ - filePath: filePath, - slashPath: slashPath, - info: info, - }) + excluded, err := isPathExcluded(filePath, excludePatternsStr) + if err != nil { + return err + } + if !excluded { + files = append(files, dirFile{ + filePath: filePath, + slashPath: slashPath, + info: info, + }) + } } return nil } @@ -152,7 +143,34 @@ func archiveProject(writer io.Writer, dir, mod, version string) error { return err } - return Create(writer, m, files) + return gozip.Create(writer, m, files) +} + +func getAbsolutePaths(exclusionPatterns []string) ([]string, error) { + var absolutedPaths []string + for _, singleExclusion := range exclusionPatterns { + singleExclusion, err := filepath.Abs(singleExclusion) + if err != nil { + return nil, err + } + absolutedPaths = append(absolutedPaths, singleExclusion) + } + return absolutedPaths, nil +} + +// This function receives a path and a regexp. +// It returns trUe is the path received matches the regexp. +// Before the match, thw path is turned into an absolute. +func isPathExcluded(path string, excludePatternsRegexp string) (excluded bool, err error) { + var fullPath string + if len(excludePatternsRegexp) > 0 { + fullPath, err = filepath.Abs(path) + if err != nil { + return + } + excluded, err = regexp.MatchString(excludePatternsRegexp, fullPath) + } + return } func isVendoredPackage(name string) bool { @@ -178,146 +196,6 @@ func isVendoredPackage(name string) bool { return strings.Contains(name[i:], "/") } -// Create builds a zip archive for module m from an abstract list of files -// and writes it to w. -// -// Create verifies the restrictions described in the package documentation -// and should not produce an archive that Unzip cannot extract. Create does not -// include files in the output archive if they don't belong in the module zip. -// In particular, Create will not include files in modules found in -// subdirectories, most files in vendor directories, or irregular files (such -// as symbolic links) in the output archive. -func Create(w io.Writer, m module.Version, files []File) (err error) { - - // Check that the version is canonical, the module path is well-formed, and - // the major version suffix matches the major version. - if vers := module.CanonicalVersion(m.Version); vers != m.Version { - if vers == "" { - vers = "the version structure to be vX.Y.Z" - } - return fmt.Errorf("version %q is not canonical (expected %s)", m.Version, vers) - } - if err := module.Check(m.Path, m.Version); err != nil { - return err - } - - // Find directories containing go.mod files (other than the root). - // These directories will not be included in the output zip. - haveGoMod := make(map[string]bool) - for _, f := range files { - dir, base := path.Split(f.Path()) - if strings.EqualFold(base, "go.mod") { - info, err := f.Lstat() - if err != nil { - return err - } - if info.Mode().IsRegular() { - haveGoMod[dir] = true - } - } - } - - inSubmodule := func(p string) bool { - for { - dir, _ := path.Split(p) - if dir == "" { - return false - } - if haveGoMod[dir] { - return true - } - p = dir[:len(dir)-1] - } - } - - // Create the module zip file. - zw := zip.NewWriter(w) - prefix := fmt.Sprintf("%s@%s/", m.Path, m.Version) - - addFile := func(f File, path string, size int64) (addFileErr error) { - var rc io.ReadCloser - var w io.Writer - rc, addFileErr = f.Open() - if addFileErr != nil { - return errorutils.CheckError(addFileErr) - } - defer func() { - closeErr := errorutils.CheckError(rc.Close()) - if addFileErr != nil { - addFileErr = closeErr - } - }() - w, addFileErr = zw.Create(prefix + path) - if addFileErr != nil { - return errorutils.CheckError(addFileErr) - } - lr := &io.LimitedReader{R: rc, N: size + 1} - if _, addFileErr = io.Copy(w, lr); addFileErr != nil { - return addFileErr - } - if lr.N <= 0 { - return errorutils.CheckErrorf("file %q is larger than declared size", path) - } - return nil - } - - collisions := make(collisionChecker) - maxSize := int64(MaxZipFile) - for _, f := range files { - p := f.Path() - if p != path.Clean(p) { - return fmt.Errorf("file path %s is not clean", p) - } - if path.IsAbs(p) { - return fmt.Errorf("file path %s is not relative", p) - } - if isVendoredPackage(p) || inSubmodule(p) { - continue - } - if p == ".hg_archival.txt" { - // Inserted by hg archive. - // The go command drops this regardless of the VCS being used. - continue - } - if err := module.CheckFilePath(p); err != nil { - return err - } - if strings.ToLower(p) == "go.mod" && p != "go.mod" { - return fmt.Errorf("found file named %s, want all lower-case go.mod", p) - } - info, err := f.Lstat() - if err != nil { - return err - } - if err := collisions.check(p, info.IsDir()); err != nil { - return err - } - if !info.Mode().IsRegular() { - // Skip symbolic links (golang.org/issue/27093). - continue - } - size := info.Size() - if size < 0 || maxSize < size { - return fmt.Errorf("module source tree too large (max size is %d bytes)", MaxZipFile) - } - maxSize -= size - if p == "go.mod" && size > MaxGoMod { - return fmt.Errorf("go.mod file too large (max size is %d bytes)", MaxGoMod) - } - if p == "LICENSE" && size > MaxLICENSE { - return fmt.Errorf("LICENSE file too large (max size is %d bytes)", MaxLICENSE) - } - - if err := addFile(f, p, size); err != nil { - return err - } - } - if err := zw.Close(); err != nil { - return err - } - return -} - type dirFile struct { filePath, slashPath string info os.FileInfo @@ -327,18 +205,6 @@ func (f dirFile) Path() string { return f.slashPath } func (f dirFile) Lstat() (os.FileInfo, error) { return f.info, nil } func (f dirFile) Open() (io.ReadCloser, error) { return os.Open(f.filePath) } -// collisionChecker finds case-insensitive name collisions and paths that -// are listed as both files and directories. -// -// The keys of this map are processed with strToFold. pathInfo has the original -// path for each folded path. -type collisionChecker map[string]pathInfo - -type pathInfo struct { - path string - isDir bool -} - // File provides an abstraction for a file in a directory, zip, or anything // else that looks like a file. type File interface { @@ -354,67 +220,3 @@ type File interface { // an error if called on a directory or symbolic link. Open() (io.ReadCloser, error) } - -func (cc collisionChecker) check(p string, isDir bool) error { - fold := strToFold(p) - if other, ok := cc[fold]; ok { - if p != other.path { - return fmt.Errorf("case-insensitive file name collision: %q and %q", other.path, p) - } - if isDir != other.isDir { - return fmt.Errorf("entry %q is both a file and a directory", p) - } - if !isDir { - return fmt.Errorf("multiple entries for file %q", p) - } - // It's not an error if check is called with the same directory multiple - // times. check is called recursively on parent directories, so check - // may be called on the same directory many times. - } else { - cc[fold] = pathInfo{path: p, isDir: isDir} - } - - if parent := path.Dir(p); parent != "." { - return cc.check(parent, true) - } - return nil -} - -// strToFold returns a string with the property that -// strings.EqualFold(s, t) iff strToFold(s) == strToFold(t) -// This lets us test a large set of strings for fold-equivalent -// duplicates without making a quadratic number of calls -// to EqualFold. Note that strings.ToUpper and strings.ToLower -// do not have the desired property in some corner cases. -func strToFold(s string) string { - // Fast path: all ASCII, no upper case. - // Most paths look like this already. - for i := 0; i < len(s); i++ { - c := s[i] - if c >= utf8.RuneSelf || 'A' <= c && c <= 'Z' { - goto Slow - } - } - return s - -Slow: - var buf bytes.Buffer - for _, r := range s { - // SimpleFold(x) cycles to the next equivalent rune > x - // or wraps around to smaller values. Iterate until it wraps, - // and we've found the minimum value. - for { - r0 := r - r = unicode.SimpleFold(r0) - if r <= r0 { - break - } - } - // Exception to allow fast path above: A-Z => a-z - if 'A' <= r && r <= 'Z' { - r += 'a' - 'A' - } - buf.WriteRune(r) - } - return buf.String() -} diff --git a/artifactory/commands/golang/archive_test.go b/artifactory/commands/golang/archive_test.go index 51e03afda..784aef05f 100644 --- a/artifactory/commands/golang/archive_test.go +++ b/artifactory/commands/golang/archive_test.go @@ -2,6 +2,7 @@ package golang import ( "bytes" + "github.com/stretchr/testify/assert" "os" "path/filepath" "reflect" @@ -23,25 +24,45 @@ func TestArchiveProject(t *testing.T) { if err != nil { t.Error(err) } - buff := &bytes.Buffer{} - if err != nil { - t.Error(err) - } originalFolder := "test_.git_suffix" baseDir, dotGitPath := tests.PrepareDotGitDir(t, originalFolder, "testdata") - err = archiveProject(buff, filepath.Join(pwd, "testdata"), "myproject.com/module/name", "v1.0.0") - if err != nil { - t.Error(err) - } - expected := map[utils.Algorithm]string{utils.MD5: "28617d6e74fce3dd2bab21b1bd65009b", utils.SHA1: "410814fbf21afdfb9c5b550151a51c2e986447fa", utils.SHA256: "e877c07315d6d3ad69139035defc08c04b400b36cd069b35ea3c2960424f2dc6"} - actual, err := utils.CalcChecksums(buff) - if err != nil { - t.Error(err) + var archiveWithExclusion = []struct { + buff *bytes.Buffer + filePath string + mod string + version string + excludedPatterns []string + expected map[utils.Algorithm]string + }{ + {buff, filepath.Join(pwd, "testdata"), "myproject.com/module/name", "v1.0.0", nil, map[utils.Algorithm]string{utils.MD5: "5b3603a7bf637622516673b845249205", utils.SHA1: "7386685c432c39428c9cb8584a2b970139c5e626", utils.SHA256: "eefd8aa3f9ac89876c8442d5feebbc837666bf40114d201219e3e6d51c208949"}}, + {buff, filepath.Join(pwd, "testdata"), "myproject.com/module/name", "v1.0.0", []string{"./testdata/dir1/*"}, map[utils.Algorithm]string{utils.MD5: "c2eeb4ef958edee91570690bf4111fc7", utils.SHA1: "d77e10eaa9bd863a9ff3775d3e452041e6f5aa40", utils.SHA256: "ecf66c1256263b2b4386efc299fa0c389263608efda9d1d91af8a746e6c5709a"}}, + {buff, filepath.Join(pwd, "testdata"), "myproject.com/module/name", "v1.0.0", []string{"./testdata/dir2/*"}, map[utils.Algorithm]string{utils.MD5: "bbe78a98ba10c1428f3a364570015e11", utils.SHA1: "99fd22ea2fe9c2c48124e741881fc3a555458a7e", utils.SHA256: "e2299f3c4e1f22d36befba191a347783dc2047e8e38cf6b9b96c273090f6e25b"}}, + {buff, filepath.Join(pwd, "testdata"), "myproject.com/module/name", "v1.0.0", []string{"./testdata/dir2/*", "testdata/dir3/*"}, map[utils.Algorithm]string{utils.MD5: "28617d6e74fce3dd2bab21b1bd65009b", utils.SHA1: "410814fbf21afdfb9c5b550151a51c2e986447fa", utils.SHA256: "e877c07315d6d3ad69139035defc08c04b400b36cd069b35ea3c2960424f2dc6"}}, + {buff, filepath.Join(pwd, "testdata"), "myproject.com/module/name", "v1.0.0", []string{"./testdata/dir2/*", "./testdata/dir3/dir4/*"}, map[utils.Algorithm]string{utils.MD5: "46a3ded48ed7998b1b35c80fbe0ffab5", utils.SHA1: "a26e73e7d29e49dd5d9c87da8f7c93cf929750df", utils.SHA256: "cf224b12eca12de4a052ef0f444519d64b6cecaf7b06050a02998be190e88847"}}, + {buff, filepath.Join(pwd, "testdata"), "myproject.com/module/name", "v1.0.0", []string{"./testdata/dir3/*"}, map[utils.Algorithm]string{utils.MD5: "c2a2dd6a7af84c2d88a48caf0c3aec34", utils.SHA1: "193d761317a602d18566561678b7bddc4773385c", utils.SHA256: "3efcd8b0d88081ec64333ff98b43616d283c4d52ed26cd7c8df646d9ea452c31"}}, + {buff, filepath.Join(pwd, "testdata"), "myproject.com/module/name", "v1.0.0", []string{"*.txt"}, map[utils.Algorithm]string{utils.MD5: "e93953b4be84d7753e0f33589b7dc4ba", utils.SHA1: "280c7492f57262b6e0af56b06c9db6a128e32ab9", utils.SHA256: "e7357986c59bf670af1e2f4868edb1406a87d328b7681b15cf038491cdc7e88c"}}, + {buff, filepath.Join(pwd, "testdata"), "myproject.com/module/name", "v1.0.0", []string{"./*/dir4/*.txt"}, map[utils.Algorithm]string{utils.MD5: "785f0c0c7b20dfd716178856edb79834", utils.SHA1: "d07204277ece1d7bef6a9f289a56afb91d66125f", utils.SHA256: "6afa0dd70bfa7c6d3aca1a3dfcd6465c542d64136c6391fa611795e6fa5800ce"}}, } + for _, testData := range archiveWithExclusion { + err = archiveProject(testData.buff, testData.filePath, testData.mod, testData.version, testData.excludedPatterns) + assert.NoError(t, err) + actual, err := utils.CalcChecksums(buff) + assert.NoError(t, err) - if !reflect.DeepEqual(expected, actual) { - t.Errorf("Expecting: %v, Got: %v", expected, actual) + if !reflect.DeepEqual(testData.expected, actual) { + t.Errorf("Expecting: %v, Got: %v", testData.expected, actual) + } } tests.RenamePath(dotGitPath, filepath.Join(baseDir, originalFolder), t) } + +func TestGetAbsolutePaths(t *testing.T) { + testData := []string{filepath.Join(".", "dir1", "*"), "*.txt", filepath.Join("*", "dir2", "*")} + result, err := getAbsolutePaths(testData) + assert.NoError(t, err) + wd, err := os.Getwd() + assert.NoError(t, err) + expectedResults := []string{filepath.Join(wd, "dir1", "*"), filepath.Join(wd, "*.txt"), filepath.Join(wd, "*", "dir2", "*")} + assert.ElementsMatch(t, result, expectedResults) +} diff --git a/artifactory/commands/golang/gopublish.go b/artifactory/commands/golang/gopublish.go index 8b114d286..a9278097f 100644 --- a/artifactory/commands/golang/gopublish.go +++ b/artifactory/commands/golang/gopublish.go @@ -17,6 +17,7 @@ type GoPublishCommandArgs struct { buildConfiguration *utils.BuildConfiguration version string detailedSummary bool + excludedPatterns []string result *commandutils.Result utils.RepositoryConfig } @@ -40,6 +41,15 @@ func (gpc *GoPublishCommand) SetConfigFilePath(configFilePath string) *GoPublish return gpc } +func (gpc *GoPublishCommand) GetExcludedPatterns() []string { + return gpc.excludedPatterns +} + +func (gpc *GoPublishCommandArgs) SetExcludedPatterns(excludedPatterns []string) *GoPublishCommandArgs { + gpc.excludedPatterns = excludedPatterns + return gpc +} + func (gpc *GoPublishCommand) Run() error { err := validatePrerequisites() if err != nil { @@ -100,7 +110,7 @@ func (gpc *GoPublishCommand) Run() error { } // Publish the package to Artifactory. - summary, artifacts, err := publishPackage(gpc.version, gpc.TargetRepo(), buildName, buildNumber, project, serviceManager) + summary, artifacts, err := publishPackage(gpc.version, gpc.TargetRepo(), buildName, buildNumber, project, gpc.GetExcludedPatterns(), serviceManager) if err != nil { return err } diff --git a/artifactory/commands/golang/publish.go b/artifactory/commands/golang/publish.go index f16bca3e9..81277d332 100644 --- a/artifactory/commands/golang/publish.go +++ b/artifactory/commands/golang/publish.go @@ -25,7 +25,7 @@ import ( ) // Publish go project to Artifactory. -func publishPackage(packageVersion, targetRepo, buildName, buildNumber, projectKey string, servicesManager artifactory.ArtifactoryServicesManager) (summary *servicesutils.OperationSummary, artifacts []buildinfo.Artifact, err error) { +func publishPackage(packageVersion, targetRepo, buildName, buildNumber, projectKey string, excludedPatterns []string, servicesManager artifactory.ArtifactoryServicesManager) (summary *servicesutils.OperationSummary, artifacts []buildinfo.Artifact, err error) { projectPath, err := goutils.GetProjectRoot() if err != nil { return nil, nil, errorutils.CheckError(err) @@ -71,7 +71,7 @@ func publishPackage(packageVersion, targetRepo, buildName, buildNumber, projectK params.ModuleId = moduleName params.ModContent = modContent params.ModPath = filepath.Join(projectPath, "go.mod") - params.ZipPath, zipArtifact, err = archive(moduleName, packageVersion, projectPath, tempDirPath) + params.ZipPath, zipArtifact, err = archive(moduleName, packageVersion, projectPath, tempDirPath, excludedPatterns) if err != nil { return nil, nil, err } @@ -182,7 +182,7 @@ func readModFile(version, projectPath string, createArtifact bool) ([]byte, *bui // Archive the go project. // Returns the path of the temp archived project file. -func archive(moduleName, version, projectPath, tempDir string) (name string, zipArtifact *buildinfo.Artifact, err error) { +func archive(moduleName, version, projectPath, tempDir string, excludedPatterns []string) (name string, zipArtifact *buildinfo.Artifact, err error) { openedFile := false tempFile, err := os.CreateTemp(tempDir, "project.zip") if err != nil { @@ -197,8 +197,7 @@ func archive(moduleName, version, projectPath, tempDir string) (name string, zip } } }() - err = archiveProject(tempFile, projectPath, moduleName, version) - if err != nil { + if err = archiveProject(tempFile, projectPath, moduleName, version, excludedPatterns); err != nil { return "", nil, errorutils.CheckError(err) } // Double-check that the paths within the zip file are well-formed. diff --git a/artifactory/commands/golang/testdata/dir2/dir2.text b/artifactory/commands/golang/testdata/dir2/dir2.text new file mode 100644 index 000000000..bf245aeae --- /dev/null +++ b/artifactory/commands/golang/testdata/dir2/dir2.text @@ -0,0 +1 @@ +dir2.text \ No newline at end of file diff --git a/artifactory/commands/golang/testdata/dir3/c.txt b/artifactory/commands/golang/testdata/dir3/c.txt new file mode 100644 index 000000000..f632129c1 --- /dev/null +++ b/artifactory/commands/golang/testdata/dir3/c.txt @@ -0,0 +1 @@ +c.txt \ No newline at end of file diff --git a/artifactory/commands/golang/testdata/dir3/dir4/dir4.txt b/artifactory/commands/golang/testdata/dir3/dir4/dir4.txt new file mode 100644 index 000000000..1c563de15 --- /dev/null +++ b/artifactory/commands/golang/testdata/dir3/dir4/dir4.txt @@ -0,0 +1 @@ +dir4.txt \ No newline at end of file diff --git a/go.mod b/go.mod index f88563ffd..4e821e68b 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230705083849-6fd087a5e228 +replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230717090738-b2e0c7bcc026 // replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20230518114837-fe6a826d5001 diff --git a/go.sum b/go.sum index 6c6d6437a..af9738bb0 100644 --- a/go.sum +++ b/go.sum @@ -198,8 +198,8 @@ github.com/jfrog/build-info-go v1.9.6 h1:lCJ2j5uXAlJsSwDe5J8WD7Co1f/hUlZvMfwfb5A github.com/jfrog/build-info-go v1.9.6/go.mod h1:GbuFS+viHCKZYx9nWHYu7ab1DgQkFdtVN3BJPUNb2D4= github.com/jfrog/gofrog v1.3.0 h1:o4zgsBZE4QyDbz2M7D4K6fXPTBJht+8lE87mS9bw7Gk= github.com/jfrog/gofrog v1.3.0/go.mod h1:IFMc+V/yf7rA5WZ74CSbXe+Lgf0iApEQLxRZVzKRUR0= -github.com/jfrog/jfrog-client-go v1.31.1 h1:lmunA5ZpRsrWTXgEGvnvVPIfwEqB3gn6+eVNpV2VBzU= -github.com/jfrog/jfrog-client-go v1.31.1/go.mod h1:qEJxoe68sUtqHJ1YhXv/7pKYP/9p1D5tJrruzJKYeoI= +github.com/jfrog/jfrog-client-go v1.28.1-0.20230717090738-b2e0c7bcc026 h1:Xam/SD9ZqanqexbX2iW2H1fH5MLB9qx6vN8SK8wBMhA= +github.com/jfrog/jfrog-client-go v1.28.1-0.20230717090738-b2e0c7bcc026/go.mod h1:qEJxoe68sUtqHJ1YhXv/7pKYP/9p1D5tJrruzJKYeoI= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -580,6 +580,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/xray/commands/scan/scan.go b/xray/commands/scan/scan.go index 8a228fde4..0bd67b67f 100644 --- a/xray/commands/scan/scan.go +++ b/xray/commands/scan/scan.go @@ -393,7 +393,7 @@ func collectPatternMatchingFiles(fileData spec.File, rootPath string, dataHandle if err != nil { return err } - excludePathPattern := fspatterns.PrepareExcludePathPattern(fileParams) + excludePathPattern := fspatterns.PrepareExcludePathPattern(fileParams.Exclusions, fileParams.GetPatternType(), fileParams.IsRecursive()) patternRegex, err := regexp.Compile(fileData.Pattern) if errorutils.CheckError(err) != nil { return err From 252fa22496782ff402b366bc2cacb815f9f89c8b Mon Sep 17 00:00:00 2001 From: zeevt-at-jfrog <139754449+zeevt-at-jfrog@users.noreply.github.com> Date: Tue, 18 Jul 2023 07:40:15 +0300 Subject: [PATCH 4/6] Change column names in curation audit command (#866) --- xray/commands/curation/audit.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xray/commands/curation/audit.go b/xray/commands/curation/audit.go index ce59134a2..161e26078 100644 --- a/xray/commands/curation/audit.go +++ b/xray/commands/curation/audit.go @@ -89,8 +89,8 @@ type PackageStatusTable struct { PkgType string `col-name:"Package\nType" auto-merge:"true"` Policy string `col-name:"Violated\nPolicy\nName"` Condition string `col-name:"Violated Condition\nName"` - Explanation string `col-name:"Explanation Name"` - Recommendation string `col-name:"Recommendation Name"` + Explanation string `col-name:"Explanation"` + Recommendation string `col-name:"Recommendation"` } type treeAnalyzer struct { From 6163244c53e5183a8dd00f4ea68fe61bd918ece4 Mon Sep 17 00:00:00 2001 From: Omer Zidkoni <50792403+omerzi@users.noreply.github.com> Date: Tue, 18 Jul 2023 13:55:24 +0300 Subject: [PATCH 5/6] Enable Secrets & IaC scanners, add Java Applicability scanning (#865) --- go.mod | 2 +- xray/audit/jas/applicabilitymanager.go | 2 +- xray/audit/jas/secretsscanner.go | 8 +++----- xray/utils/analyzermanager.go | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 4e821e68b..43635b986 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/jedib0t/go-pretty/v6 v6.4.6 github.com/jfrog/build-info-go v1.9.6 github.com/jfrog/gofrog v1.3.0 - github.com/jfrog/jfrog-client-go v1.31.1 + github.com/jfrog/jfrog-client-go v1.31.2 github.com/magiconair/properties v1.8.7 github.com/manifoldco/promptui v0.9.0 github.com/owenrumney/go-sarif/v2 v2.1.3 diff --git a/xray/audit/jas/applicabilitymanager.go b/xray/audit/jas/applicabilitymanager.go index c7f541972..d6d96c79b 100644 --- a/xray/audit/jas/applicabilitymanager.go +++ b/xray/audit/jas/applicabilitymanager.go @@ -27,7 +27,7 @@ const ( var ( technologiesEligibleForApplicabilityScan = []coreutils.Technology{coreutils.Npm, coreutils.Pip, - coreutils.Poetry, coreutils.Pipenv, coreutils.Pypi} + coreutils.Poetry, coreutils.Pipenv, coreutils.Pypi, coreutils.Maven, coreutils.Gradle, coreutils.Yarn} ) // The getApplicabilityScanResults function runs the applicability scan flow, which includes the following steps: diff --git a/xray/audit/jas/secretsscanner.go b/xray/audit/jas/secretsscanner.go index 9d1a26921..972c6fa6e 100644 --- a/xray/audit/jas/secretsscanner.go +++ b/xray/audit/jas/secretsscanner.go @@ -3,6 +3,9 @@ package jas import ( "errors" "fmt" + "os" + "path/filepath" + "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" @@ -11,13 +14,10 @@ import ( "github.com/jfrog/jfrog-client-go/utils/log" "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" ) @@ -105,7 +105,6 @@ type secretsScanConfiguration struct { Roots []string `yaml:"roots"` Output string `yaml:"output"` Type string `yaml:"type"` - Scanners string `yaml:"scanners"` SkippedDirs []string `yaml:"skipped-folders"` } @@ -121,7 +120,6 @@ func (s *SecretScanManager) createConfigFile() error { Roots: []string{currentDir}, Output: s.resultsFileName, Type: secretsScannerType, - Scanners: secretsScannersNames, SkippedDirs: skippedDirs, }, }, diff --git a/xray/utils/analyzermanager.go b/xray/utils/analyzermanager.go index b21c24937..66ed08d26 100644 --- a/xray/utils/analyzermanager.go +++ b/xray/utils/analyzermanager.go @@ -26,7 +26,7 @@ const ( EntitlementsMinVersion = "3.66.5" ApplicabilityFeatureId = "contextual_analysis" AnalyzerManagerZipName = "analyzerManager.zip" - analyzerManagerVersion = "1.1.9.1786834" + analyzerManagerVersion = "1.2.2.1846078" analyzerManagerDownloadPath = "xsc-gen-exe-analyzer-manager-local/v1" analyzerManagerDirName = "analyzerManager" analyzerManagerExecutableName = "analyzerManager" From 056757c17c3e408b5c74e24130455987fafa1155 Mon Sep 17 00:00:00 2001 From: Omer Zidkoni <50792403+omerzi@users.noreply.github.com> Date: Tue, 18 Jul 2023 14:14:49 +0300 Subject: [PATCH 6/6] Upgrade jfrog-cli-core to 2.39.0 (#868) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 43635b986..0d5286cd3 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230717090738-b2e0c7bcc026 +// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230717090738-b2e0c7bcc026 // replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20230518114837-fe6a826d5001 diff --git a/go.sum b/go.sum index af9738bb0..f89d7a07b 100644 --- a/go.sum +++ b/go.sum @@ -198,8 +198,8 @@ github.com/jfrog/build-info-go v1.9.6 h1:lCJ2j5uXAlJsSwDe5J8WD7Co1f/hUlZvMfwfb5A github.com/jfrog/build-info-go v1.9.6/go.mod h1:GbuFS+viHCKZYx9nWHYu7ab1DgQkFdtVN3BJPUNb2D4= github.com/jfrog/gofrog v1.3.0 h1:o4zgsBZE4QyDbz2M7D4K6fXPTBJht+8lE87mS9bw7Gk= github.com/jfrog/gofrog v1.3.0/go.mod h1:IFMc+V/yf7rA5WZ74CSbXe+Lgf0iApEQLxRZVzKRUR0= -github.com/jfrog/jfrog-client-go v1.28.1-0.20230717090738-b2e0c7bcc026 h1:Xam/SD9ZqanqexbX2iW2H1fH5MLB9qx6vN8SK8wBMhA= -github.com/jfrog/jfrog-client-go v1.28.1-0.20230717090738-b2e0c7bcc026/go.mod h1:qEJxoe68sUtqHJ1YhXv/7pKYP/9p1D5tJrruzJKYeoI= +github.com/jfrog/jfrog-client-go v1.31.2 h1:foy8owM2lS8jZL7zuBPtcx1RpF1GeIXaXF8hIufyr4I= +github.com/jfrog/jfrog-client-go v1.31.2/go.mod h1:qEJxoe68sUtqHJ1YhXv/7pKYP/9p1D5tJrruzJKYeoI= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=