diff --git a/artifactory_test.go b/artifactory_test.go index ee50c8020..0c3f8d130 100644 --- a/artifactory_test.go +++ b/artifactory_test.go @@ -238,7 +238,7 @@ func TestDownloadAnalyzerManagerIfNeeded(t *testing.T) { defer setEnvCallBack() // Download - err := jas.DownloadAnalyzerManagerIfNeeded(0) + err := jas.DownloadAnalyzerManagerIfNeeded("", nil, 0) assert.NoError(t, err) // Validate Analyzer manager app & checksum.sh2 file exist @@ -259,7 +259,7 @@ func TestDownloadAnalyzerManagerIfNeeded(t *testing.T) { // Validate no second download occurred firstFileStat, err := os.Stat(amPath) assert.NoError(t, err) - err = jas.DownloadAnalyzerManagerIfNeeded(0) + err = jas.DownloadAnalyzerManagerIfNeeded("", nil, 0) assert.NoError(t, err) secondFileStat, err := os.Stat(amPath) assert.NoError(t, err) diff --git a/audit_test.go b/audit_test.go index 3a405f77b..1153d59cf 100644 --- a/audit_test.go +++ b/audit_test.go @@ -64,6 +64,8 @@ type auditCommandTestParams struct { WithSbom bool // adds "--secrets", "--validate-secrets" flags if true ValidateSecrets bool + // adds "--sca", "--secrets", "--iac", "--sast" flags if provided + OnlyScan []utils.SubScanType // adds "--sca" and "--without-contextual-analysis" flags if true OnlyScaScan bool // adds "--static-sca" flag value if provided @@ -107,7 +109,10 @@ func getAuditCmdArgs(params auditCommandTestParams) (args []string) { args = append(args, "--vuln") } if params.ValidateSecrets { - args = append(args, "--secrets", "--validate-secrets") + args = append(args, "--validate-secrets") + } + if len(params.OnlyScan) > 0 { + args = append(args, subScansToFlags(params.OnlyScan)...) } if params.WithSbom { args = append(args, "--sbom") @@ -776,6 +781,7 @@ func TestXrayAuditJasSimpleJson(t *testing.T) { func TestXrayAuditJasSimpleJsonWithTokenValidation(t *testing.T) { securityIntegrationTestUtils.InitAuditGeneralTests(t, jasutils.DynamicTokenValidationMinXrayVersion) output := testXrayAuditWithCleanHome(t, securityTests.PlatformCli, filepath.Join("jas", "jas"), auditCommandTestParams{ + OnlyScan: []utils.SubScanType{utils.SecretsScan}, ValidateSecrets: true, Format: format.SimpleJson, }) @@ -964,10 +970,14 @@ func testXrayAuditGem(t *testing.T, format string) string { // New Sca -func testAuditCommandNewSca(t *testing.T, project string, params auditCommandTestParams) (string, error) { +func testAuditCommandNewSca(t *testing.T, params auditCommandTestParams, projects ...string) (string, error) { // Must have one target, in new SCA mode the flow should not 'dirty' the local environment // No need to copy or change directories just point to the project directory - params.WorkingDirsToScan = []string{filepath.Join(filepath.FromSlash(securityTests.GetTestResourcesPath()), "projects", project)} + if len(params.WorkingDirsToScan) == 0 { + for _, project := range projects { + params.WorkingDirsToScan = append(params.WorkingDirsToScan, filepath.Join(filepath.FromSlash(securityTests.GetTestResourcesPath()), "projects", project)) + } + } params.WithStaticSca = true // No **/tests/** exclusion, we are scanning projects in the test resources path params.CustomExclusion = []string{"*.git*", "*node_modules*", "*target*", "*venv*", "dist"} @@ -983,10 +993,12 @@ func testAuditCommandNewSca(t *testing.T, project string, params auditCommandTes func TestAuditNewScaCycloneDxNpm(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) - output, err := testAuditCommandNewSca(t, filepath.Join("jas", "jas-npm"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, Format: format.CycloneDx, - }) + }, + filepath.Join("jas", "jas-npm"), + ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ ExactResultsMatch: true, @@ -999,6 +1011,25 @@ func TestAuditNewScaCycloneDxNpm(t *testing.T) { }) } +func TestAuditNewScaSimpleJsonMultipleWorkingDirs(t *testing.T) { + securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ + WithSbom: true, + Format: format.SimpleJson, + }, + filepath.Join("jas", "jas-npm"), + filepath.Join("package-managers", "go", "simple-project"), + ) + assert.NoError(t, err) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + ExactResultsMatch: true, + Total: &validations.TotalCount{Vulnerabilities: 10}, + Vulnerabilities: &validations.VulnerabilityCount{ + ValidateScan: &validations.ScanCount{Sca: 7, Sast: 2, Secrets: 1}, + }, + }) +} + func TestAuditNewScaSimpleJsonViolations(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) @@ -1007,13 +1038,15 @@ func TestAuditNewScaSimpleJsonViolations(t *testing.T) { watchName, deleteWatch := securityTestUtils.CreateWatchOnArtifactoryRepos(t, policyName, "static-sca-watch", xrayUtils.Security) defer deleteWatch() - output, err := testAuditCommandNewSca(t, filepath.Join("jas", "jas-npm"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, WithVuln: true, WithLicense: true, Format: format.SimpleJson, Watches: []string{watchName}, - }) + }, + filepath.Join("jas", "jas-npm"), + ) // Make Sure to check violations with fail build error assert.Equal(t, err, policy.NewFailBuildError()) // Validate results @@ -1033,10 +1066,12 @@ func TestAuditNewScaSimpleJsonViolations(t *testing.T) { func TestAuditNewScaCycloneDxPnpm(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) - output, err := testAuditCommandNewSca(t, filepath.Join("package-managers", "npm", "pnpm-lock"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, Format: format.CycloneDx, - }) + }, + filepath.Join("package-managers", "npm", "pnpm-lock"), + ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ ExactResultsMatch: true, @@ -1051,11 +1086,13 @@ func TestAuditNewScaCycloneDxPnpm(t *testing.T) { func TestAuditNewScaCycloneDxMaven(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) - output, err := testAuditCommandNewSca(t, filepath.Join("package-managers", "maven", "maven-example"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, Threads: 5, Format: format.CycloneDx, - }) + }, + filepath.Join("package-managers", "maven", "maven-example"), + ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ ExactResultsMatch: true, @@ -1070,10 +1107,12 @@ func TestAuditNewScaCycloneDxMaven(t *testing.T) { func TestAuditNewScaCycloneDxGradle(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) - output, err := testAuditCommandNewSca(t, filepath.Join("package-managers", "gradle", "gradle-lock"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, Format: format.CycloneDx, - }) + }, + filepath.Join("package-managers", "gradle", "gradle-lock"), + ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ ExactResultsMatch: true, @@ -1088,10 +1127,12 @@ func TestAuditNewScaCycloneDxGradle(t *testing.T) { func TestAuditNewScaCycloneDxGo(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) - output, err := testAuditCommandNewSca(t, filepath.Join("package-managers", "go", "simple-project"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, Format: format.CycloneDx, - }) + }, + filepath.Join("package-managers", "go", "simple-project"), + ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ ExactResultsMatch: true, @@ -1106,10 +1147,12 @@ func TestAuditNewScaCycloneDxGo(t *testing.T) { func TestAuditNewScaCycloneDxYarn(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) - output, err := testAuditCommandNewSca(t, filepath.Join("package-managers", "yarn", "yarn-v3"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, Format: format.CycloneDx, - }) + }, + filepath.Join("package-managers", "yarn", "yarn-v3"), + ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ ExactResultsMatch: true, @@ -1124,10 +1167,12 @@ func TestAuditNewScaCycloneDxYarn(t *testing.T) { func TestAuditNewScaCycloneDxPip(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) - output, err := testAuditCommandNewSca(t, filepath.Join("jas", "jas"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, Format: format.CycloneDx, - }) + }, + filepath.Join("jas", "jas"), + ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ ExactResultsMatch: true, @@ -1141,10 +1186,12 @@ func TestAuditNewScaCycloneDxPip(t *testing.T) { func TestAuditNewScaCycloneDxPoetry(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) - output, err := testAuditCommandNewSca(t, filepath.Join("package-managers", "python", "poetry", "poetry-project"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, Format: format.CycloneDx, - }) + }, + filepath.Join("package-managers", "python", "poetry", "poetry-project"), + ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ ExactResultsMatch: true, @@ -1159,10 +1206,12 @@ func TestAuditNewScaCycloneDxPoetry(t *testing.T) { func TestAuditNewScaCycloneDxPipenv(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) - output, err := testAuditCommandNewSca(t, filepath.Join("package-managers", "python", "pipenv", "pipenv-lock"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, Format: format.CycloneDx, - }) + }, + filepath.Join("package-managers", "python", "pipenv", "pipenv-lock"), + ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ ExactResultsMatch: true, @@ -1177,10 +1226,12 @@ func TestAuditNewScaCycloneDxPipenv(t *testing.T) { func TestAuditNewScaCycloneDxUV(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) - output, err := testAuditCommandNewSca(t, filepath.Join("package-managers", "python", "uv", "uv"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, Format: format.CycloneDx, - }) + }, + filepath.Join("package-managers", "python", "uv", "uv"), + ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ ExactResultsMatch: true, @@ -1195,10 +1246,12 @@ func TestAuditNewScaCycloneDxUV(t *testing.T) { func TestAuditNewScaCycloneDxNuget(t *testing.T) { securityIntegrationTestUtils.InitAuditNewScaTests(t, utils.StaticScanMinVersion) - output, err := testAuditCommandNewSca(t, filepath.Join("package-managers", "nuget", "single4.0"), auditCommandTestParams{ + output, err := testAuditCommandNewSca(t, auditCommandTestParams{ WithSbom: true, Format: format.CycloneDx, - }) + }, + filepath.Join("package-managers", "nuget", "single4.0"), + ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ ExactResultsMatch: true, @@ -1224,14 +1277,14 @@ func TestAuditNewScaSnippetDetection(t *testing.T) { Watches: []string{watchName}, } // No snippet detection. nothing should be found - output, err := testAuditCommandNewSca(t, filepath.Join("package-managers", "c", "snippet_detection"), params) + output, err := testAuditCommandNewSca(t, params, filepath.Join("package-managers", "c", "snippet_detection")) assert.NoError(t, err) validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ExactResultsMatch: true}, ) // With snippet detection. should find 4 licenses violations params.WithSnippetDetection = true - output, err = testAuditCommandNewSca(t, filepath.Join("package-managers", "c", "snippet_detection"), params) + output, err = testAuditCommandNewSca(t, params, filepath.Join("package-managers", "c", "snippet_detection")) assert.NoError(t, err) validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ diff --git a/cli/docs/flags.go b/cli/docs/flags.go index 5d358adc2..f3723c3f3 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -177,6 +177,8 @@ const ( DetailedSummary = "detailed-summary" CacheValidity = "cache-validity" GitThreads = gitPrefix + Threads + + UseConfigProfile = "use-config-profile" ) // Mapping between security commands (key) and their flags (key). @@ -217,8 +219,8 @@ var commandFlags = map[string][]string{ // Violations params scanProjectKey, Watches, Snippet, ScanVuln, Fail, // Scan params - Threads, ExclusionsAudit, - auditSca, auditIac, auditSast, auditSecrets, auditWithoutCA, SecretValidation, Sbom, + Threads, ExclusionsAudit, WorkingDirs, + auditSca, auditIac, auditSast, auditSecrets, auditWithoutCA, SecretValidation, Sbom, UseConfigProfile, // Output params Licenses, OutputFormat, ExtendedTable, OutputDir, UploadRtRepoPath, // Scan Logic params @@ -363,6 +365,8 @@ var flagsMap = map[string]components.Flag{ AddSastRules: components.NewStringFlag(AddSastRules, "Incorporate any additional SAST rules (in JSON format, with absolute path) into this local scan."), Port: components.NewStringFlag(Port, "Specifies the port to run the SAST server on.", components.SetMandatory()), + UseConfigProfile: components.NewBoolFlag(UseConfigProfile, "Set to false to override config profile for the audit.", components.WithBoolDefaultValue(true), components.SetHiddenBoolFlag()), + // Docker flags DockerImageName: components.NewStringFlag(DockerImageName, "Specifies the Docker image name to audit. Uses the same format as the Docker CLI, including Artifactory-hosted images."), diff --git a/cli/gitcommands.go b/cli/gitcommands.go index cc2aff8bc..d8097fe40 100644 --- a/cli/gitcommands.go +++ b/cli/gitcommands.go @@ -55,6 +55,8 @@ func GitAuditCmd(c *components.Context) error { return err } gitAuditCmd.SetServerDetails(serverDetails).SetXrayVersion(xrayVersion).SetXscVersion(xscVersion) + // Set config profile params + gitAuditCmd.SetUseConfigProfile(c.GetBoolFlagValue(flags.UseConfigProfile)) // Set violations params format, err := outputFormat.ParseOutputFormat(c.GetStringFlagValue(flags.OutputFormat), outputFormat.All) if err != nil { diff --git a/cli/scancommands.go b/cli/scancommands.go index f74abfa44..ee52404ea 100644 --- a/cli/scancommands.go +++ b/cli/scancommands.go @@ -556,14 +556,12 @@ func CreateAuditCmd(c *components.Context) (string, string, *coreConfig.ServerDe return "", "", nil, nil, err } auditCmd.SetBomGenerator(sbomGenerator).SetCustomBomGenBinaryPath(c.GetStringFlagValue(flags.XrayLibPluginBinaryCustomPath)) - auditCmd.SetScaScanStrategy(scaScanStrategy) - auditCmd.SetViolationGenerator(violationGenerator) + auditCmd.SetScaScanStrategy(scaScanStrategy).SetViolationGenerator(violationGenerator).SetIncludeSbom(shouldIncludeSbom(c, format)) auditCmd.SetUploadCdxResults(uploadResults).SetRtResultRepository(c.GetStringFlagValue(flags.UploadRtRepoPath)) auditCmd.SetTargetRepoPath(addTrailingSlashToRepoPathIfNeeded(c)). SetProject(getProject(c)). SetIncludeVulnerabilities(c.GetBoolFlagValue(flags.Vuln)). SetIncludeLicenses(c.GetBoolFlagValue(flags.Licenses)). - SetIncludeSbom(shouldIncludeSbom(c, format)). SetIncludeSnippetDetection(includeSnippetDetection). SetFail(c.GetBoolFlagValue(flags.Fail)). SetPrintExtendedTable(c.GetBoolFlagValue(flags.ExtendedTable)). diff --git a/cli/utils.go b/cli/utils.go index 7772e1b28..eab4b48d2 100644 --- a/cli/utils.go +++ b/cli/utils.go @@ -149,7 +149,7 @@ func validateSnippetDetection(c *components.Context) (bool, error) { return false, err } // Make sure SCA is requested or SBOM is requested - if !utils.IsScanRequested(utils.SourceCode, utils.ScaScan, subScans...) && !c.GetBoolFlagValue(flags.Sbom) { + if !utils.IsScanRequested(utils.SourceCode, utils.ScaScan, nil, subScans...) && !c.GetBoolFlagValue(flags.Sbom) { return false, errorutils.CheckErrorf("Snippet detection is only supported when SCA is requested or SBOM is requested") } return true, nil diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 4c12c492b..568a58d2a 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -4,9 +4,11 @@ import ( "errors" "fmt" "os" + "path/filepath" "strconv" "strings" + "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/gofrog/parallel" "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-core/v2/utils/config" @@ -22,7 +24,6 @@ import ( "github.com/jfrog/jfrog-cli-security/policy/local" "github.com/jfrog/jfrog-cli-security/sca/bom" "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo" - "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies" "github.com/jfrog/jfrog-cli-security/sca/bom/xrayplugin" "github.com/jfrog/jfrog-cli-security/sca/bom/xrayplugin/plugin" "github.com/jfrog/jfrog-cli-security/sca/scan" @@ -42,7 +43,7 @@ import ( "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray" "github.com/jfrog/jfrog-client-go/xray/services" - xscservices "github.com/jfrog/jfrog-client-go/xsc/services" + xscServices "github.com/jfrog/jfrog-client-go/xsc/services" xscutils "github.com/jfrog/jfrog-client-go/xsc/services/utils" xrayutils "github.com/jfrog/jfrog-cli-security/utils/xray" @@ -55,7 +56,6 @@ type AuditCommand struct { targetRepoPath string IncludeVulnerabilities bool IncludeLicenses bool - IncludeSbom bool IncludeSnippetDetection bool Fail bool PrintExtendedTable bool @@ -100,12 +100,20 @@ func (auditCmd *AuditCommand) SetIncludeLicenses(include bool) *AuditCommand { auditCmd.IncludeLicenses = include return auditCmd } - -func (auditCmd *AuditCommand) SetIncludeSbom(include bool) *AuditCommand { - auditCmd.IncludeSbom = include - return auditCmd +func logScanPaths(workingDirs []string, isRecursiveScan bool) { + switch { + case len(workingDirs) > 1: + log.Info("Scanning paths:", strings.Join(workingDirs, ", ")) + case isRecursiveScan && len(workingDirs) == 0: + log.Info("Detecting recursively targets for scan in current directory") + case isRecursiveScan: + log.Info("Detecting recursively targets for scan in path:", workingDirs[0]) + case len(workingDirs) == 0: + log.Debug("Scanning current directory...") + default: + log.Info("Scanning path:", workingDirs[0]) + } } - func (auditCmd *AuditCommand) SetIncludeSnippetDetection(include bool) *AuditCommand { auditCmd.IncludeSnippetDetection = include return auditCmd @@ -187,43 +195,39 @@ func shouldIncludeSnippetDetection(params *AuditParams) bool { return strings.ToLower(os.Getenv(plugin.SnippetDetectionEnvVariable)) == "true" } -func logScanPaths(workingDirs []string, isRecursiveScan bool) { - if len(workingDirs) == 0 { +func GetTargetsInfo(workingDirs []string, bomGenerator bom.SbomGenerator, scansToPerform []utils.SubScanType, includeSbom bool, rootDir string) (projectPath string, includeDirs []string, isRecursiveScan bool, err error) { + includeDirs, err = utils.GetFullPathsWorkingDirs(workingDirs) + if err != nil { return } - if len(workingDirs) == 1 { - if isRecursiveScan { - log.Info("Detecting recursively targets for scan in path:", workingDirs[0]) + if !isNewFlow(bomGenerator) && (utils.IsScanRequested(utils.SourceCode, utils.ScaScan, nil, scansToPerform...) || includeSbom) { + // Only in case of SCA scan / SBOM requested and if no workingDirs were provided by the user + // We apply a recursive scan on the root repository + isRecursiveScan = len(workingDirs) == 0 + } + logScanPaths(includeDirs, isRecursiveScan) + if rootDir != "" { + projectPath = rootDir + } else { + if currentDir, e := coreutils.GetWorkingDirectory(); e != nil { + log.Warn(fmt.Sprintf("Failed to get working directory: %s", e.Error())) + projectPath = utils.GetCommonParentDir(includeDirs...) } else { - log.Info("Scanning path:", workingDirs[0]) + projectPath = currentDir } - return } - log.Info("Scanning paths:", strings.Join(workingDirs, ", ")) + return } -func getRelatedWorkingDirs(auditCmd *AuditCommand) (projectPath string, workingDirs []string, isRecursiveScan bool, err error) { - if _, ok := auditCmd.bomGenerator.(*xrayplugin.XrayLibBomGenerator); ok { - if len(auditCmd.workingDirs) > 1 { - return "", nil, false, errors.New("the 'audit' command with the 'Xray lib' BOM generator supports only one working directory. Please provide a single working directory") - } - // OLD logic: - } else if utils.IsScanRequested(utils.SourceCode, utils.ScaScan, auditCmd.ScansToPerform()...) || auditCmd.IncludeSbom { - // Only in case of SCA scan / SBOM requested and if no workingDirs were provided by the user - // We apply a recursive scan on the root repository - isRecursiveScan = len(auditCmd.workingDirs) == 0 - } - workingDirs, err = coreutils.GetFullPathsWorkingDirs(auditCmd.workingDirs) - if err != nil { - return +func isNewFlow(bomGenerator bom.SbomGenerator) bool { + if _, ok := bomGenerator.(*xrayplugin.XrayLibBomGenerator); ok { + return true } - logScanPaths(workingDirs, isRecursiveScan) - projectPath = utils.GetCommonParentDir(workingDirs...) - return + return false } func (auditCmd *AuditCommand) Run() (err error) { - projectPath, workingDirs, isRecursiveScan, err := getRelatedWorkingDirs(auditCmd) + projectPath, includeDirs, isRecursiveScan, err := GetTargetsInfo(auditCmd.workingDirs, auditCmd.bomGenerator, auditCmd.scansToPerform, auditCmd.resultsContext.IncludeSbom, auditCmd.rootDir) if err != nil { return } @@ -236,7 +240,7 @@ func (auditCmd *AuditCommand) Run() (err error) { auditCmd.GetXrayVersion(), auditCmd.GetXscVersion(), serverDetails, - xsc.CreateAnalyticsEvent(xscservices.CliProduct, xscservices.CliEventType, serverDetails, projectPath), + xsc.CreateAnalyticsEvent(xscServices.CliProduct, xscServices.CliEventType, serverDetails, projectPath), auditCmd.projectKey, ) @@ -248,10 +252,10 @@ func (auditCmd *AuditCommand) Run() (err error) { SetViolationGenerator(auditCmd.violationGenerator). SetRtResultRepository(auditCmd.rtResultRepository). SetUploadCdxResults(auditCmd.uploadCdxResults). - SetWorkingDirs(workingDirs). + SetWorkingDirs(includeDirs). SetMinSeverityFilter(auditCmd.minSeverityFilter). SetFixableOnly(auditCmd.fixableOnly). - SetGraphBasicParams(auditCmd.AuditBasicParams). + SetGraphBasicParams(auditCmd.AuditBasicParams.SetIsRecursiveScan(isRecursiveScan).SetExclusions(auditCmd.Exclusions())). SetResultsContext(CreateAuditResultsContext( serverDetails, auditCmd.GetXrayVersion(), @@ -261,15 +265,16 @@ func (auditCmd *AuditCommand) Run() (err error) { auditCmd.gitRepoHttpsCloneUrl, auditCmd.IncludeVulnerabilities, auditCmd.IncludeLicenses, - auditCmd.IncludeSbom, + auditCmd.resultsContext.IncludeSbom, auditCmd.IncludeSnippetDetection, )). SetGitContext(auditCmd.GitContext()). SetThirdPartyApplicabilityScan(auditCmd.thirdPartyApplicabilityScan). SetThreads(auditCmd.Threads). - SetScansResultsOutputDir(auditCmd.scanResultsOutputDir).SetStartTime(startTime).SetMultiScanId(multiScanId). + SetScansResultsOutputDir(auditCmd.scanResultsOutputDir). + SetStartTime(startTime). + SetMultiScanId(multiScanId). SetRootDir(auditCmd.rootDir).SetSastChangedFilesMode(auditCmd.sastChangedFilesMode).SetSastRules(auditCmd.sastRules) - auditParams.SetIsRecursiveScan(isRecursiveScan).SetExclusions(auditCmd.Exclusions()) auditResults := RunAudit(auditParams) @@ -330,11 +335,11 @@ func (auditCmd *AuditCommand) CommandName() string { // If the current server is entitled for JAS, the advanced security results will be included in the scan results. func RunAudit(auditParams *AuditParams) (cmdResults *results.SecurityCommandResults) { // Prepare the command for the scan. - if cmdResults = prepareToScan(auditParams); cmdResults.GeneralError != nil { + if cmdResults = prepareToScan(auditParams); cmdResults.GetErrors() != nil { return } // Run Scanners - if runParallelAuditScans(cmdResults, auditParams); cmdResults.GeneralError != nil { + if runParallelAuditScans(cmdResults, auditParams); cmdResults.GetErrors() != nil { return } // Process the scan results and run additional steps if needed. @@ -346,23 +351,23 @@ func prepareToScan(params *AuditParams) (cmdResults *results.SecurityCommandResu params.Progress().SetHeadlineMsg("Preparing to scan") } // Initialize Results struct - if cmdResults = initAuditCmdResults(params); cmdResults.GeneralError != nil { + if cmdResults = initAuditCmdResults(params); cmdResults.GetErrors() != nil { return } bomGenOptions, scanOptions, err := getScanLogicOptions(params) if err != nil { - return cmdResults.AddGeneralError(fmt.Errorf("failed to get scan logic options: %s", err.Error()), params.AllowPartialResults()) + return cmdResults.AddGeneralError(fmt.Errorf("failed to get scan logic options: %s", err.Error()), cmdResults.AllowPartialResults) } // Initialize the BOM generator if needed - if params.resultsContext.IncludeSbom || utils.IsScanRequested(cmdResults.CmdType, utils.ScaScan, params.scansToPerform...) { + if params.resultsContext.IncludeSbom || utils.IsScanRequested(cmdResults.CmdType, utils.ScaScan, cmdResults.IsScanRequestedByCentralConfig(utils.ScaScan), params.scansToPerform...) { if err = params.bomGenerator.WithOptions(bomGenOptions...).PrepareGenerator(); err != nil { - return cmdResults.AddGeneralError(fmt.Errorf("failed to prepare the BOM generator: %s", err.Error()), params.AllowPartialResults()) + return cmdResults.AddGeneralError(fmt.Errorf("failed to prepare the BOM generator: %s", err.Error()), cmdResults.AllowPartialResults) } } populateScanTargets(cmdResults, params) // Initialize the SCA scan strategy if err = params.scaScanStrategy.WithOptions(scanOptions...).PrepareStrategy(); err != nil { - return cmdResults.AddGeneralError(fmt.Errorf("failed to prepare the SCA scan strategy: %s", err.Error()), params.AllowPartialResults()) + return cmdResults.AddGeneralError(fmt.Errorf("failed to prepare the SCA scan strategy: %s", err.Error()), cmdResults.AllowPartialResults) } return } @@ -373,24 +378,25 @@ func getScanLogicOptions(params *AuditParams) (bomGenOptions []bom.SbomGenerator if err != nil { return nil, nil, fmt.Errorf("failed to create build info params: %w", err) } + serverDetails, err := params.ServerDetails() + if err != nil { + return nil, nil, fmt.Errorf("failed to get server details: %w", err) + } bomGenOptions = []bom.SbomGeneratorOption{ // Build Info Bom Generator Options buildinfo.WithParams(buildParams), // Xray-Scan-Plugin Bom Generator Options - xrayplugin.WithTotalTargets(len(params.workingDirs)), xrayplugin.WithBinaryPath(params.CustomBomGenBinaryPath()), - xrayplugin.WithIgnorePatterns(params.Exclusions()), xrayplugin.WithSpecificTechnologies(params.Technologies()), } + if params.configProfile != nil && params.configProfile.GeneralConfig.ScannersDownloadPath != "" { + bomGenOptions = append(bomGenOptions, xrayplugin.WithCentralRemoteReleasesDetails(serverDetails, params.configProfile.GeneralConfig.ScannersDownloadPath)) + } // Scan Strategies Options scanGraphParams, err := params.ToXrayScanGraphParams() if err != nil { return nil, nil, fmt.Errorf("failed to create scan graph params: %w", err) } - serverDetails, err := params.ServerDetails() - if err != nil { - return nil, nil, fmt.Errorf("failed to get server details: %w", err) - } scanOptions = []scan.SbomScanOption{ // Xray Scan Graph Strategy Options scanGraphStrategy.WithParams(scanGraphParams), @@ -409,6 +415,7 @@ func initAuditCmdResults(params *AuditParams) (cmdResults *results.SecurityComma cmdResults.SetStartTime(params.StartTime()) cmdResults.SetResultsContext(params.resultsContext) cmdResults.SetGitContext(params.GitContext()) + cmdResults.SetAllowPartialResults(params.CalculatedAllowPartialResults()) serverDetails, err := params.ServerDetails() if err != nil { return cmdResults.AddGeneralError(err, false) @@ -425,7 +432,7 @@ func initAuditCmdResults(params *AuditParams) (cmdResults *results.SecurityComma cmdResults.SetEntitledForJas(entitledForJas) if entitledForJas { // Validate required installed software - if utils.IsJASRequested(cmdResults.CmdType, params.ScansToPerform()...) { + if cmdResults.IsJASRequested(params.ScansToPerform()...) { if err = jas.ValidateRequiredInstalledSoftware(); err != nil { return cmdResults.AddGeneralError(err, false) } @@ -464,22 +471,12 @@ func isEntitledForSnippetDetection(isEntitledForJas bool, xrayManager *xray.Xray } func populateScanTargets(cmdResults *results.SecurityCommandResults, params *AuditParams) { - // Populate the scan targets based on the provided parameters. + // Populate x scan targets based on the provided parameters. detectScanTargets(cmdResults, params) - // Load apps config information - jfrogAppsConfig, err := jas.CreateJFrogAppsConfig(cmdResults.GetTargetsPaths()) - if err != nil { - cmdResults.AddGeneralError(fmt.Errorf("failed to create JFrogAppsConfig: %s", err.Error()), false) - return - } // Populate target information for the scans for _, targetResult := range cmdResults.Targets { - // Get the apps config module and assign it to the target result for JAS scans. - targetResult.AppsConfigModule = jas.GetModule(targetResult.Target, jfrogAppsConfig) // Generate SBOM for the target if requested or for SCA scans. - if !shouldGenerateSbom(params) { - // No need to generate the SBOM if we are not going to use it. - log.Debug(fmt.Sprintf("No need to generate the SBOM for %s as requested by input...", targetResult.Target)) + if !shouldGenerateSbom(targetResult, params) { continue } bom.GenerateSbomForTarget(params.BomGenerator().WithOptions( @@ -488,7 +485,8 @@ func populateScanTargets(cmdResults *results.SecurityCommandResults, params *Aud ), bom.SbomGeneratorParams{ Target: targetResult, - AllowPartialResults: params.AllowPartialResults(), + TotalTargets: len(cmdResults.Targets), + AllowPartialResults: cmdResults.AllowPartialResults, ScanResultsOutputDir: params.scanResultsOutputDir, // Diff mode - SCA DiffMode: params.DiffMode(), @@ -499,41 +497,64 @@ func populateScanTargets(cmdResults *results.SecurityCommandResults, params *Aud logScanTargetsInfo(cmdResults) } -func shouldGenerateSbom(params *AuditParams) bool { +func shouldGenerateSbom(targetResult *results.TargetResults, params *AuditParams) bool { if params.resultsContext.IncludeSbom { + log.Verbose("Sbom is requested by input...") return true } scansToPerform := params.ScansToPerform() if slices.Contains(scansToPerform, utils.ScaScan) { + log.Verbose("Sbom is requested for SCA scan...") return true } - if params.configProfile != nil && len(params.configProfile.Modules) > 0 { - return params.configProfile.Modules[0].ScanConfig.ScaScannerConfig.EnableScaScan + if targetResult != nil { + if centralConfiguredToRun := targetResult.IsScanRequestedByCentralConfig(utils.ScaScan); centralConfiguredToRun != nil { + profileName := "" + if params.configProfile != nil { + profileName = params.configProfile.ProfileName + } + log.Debug(fmt.Sprintf("Using config profile '%s' to determine if SBOM should be generated...", profileName)) + if !*centralConfiguredToRun { + log.Debug(fmt.Sprintf("Skipping SBOM generation as SCA scan is not requested by '%s' config profile...", profileName)) + return false + } + return true + } } - return len(scansToPerform) == 0 + if configProfile := params.GetConfigProfile(); configProfile != nil && len(configProfile.Modules) > 0 { + enableSca := configProfile.Modules[0].ScanConfig.ScaScannerConfig.EnableScaScan + log.Debug(fmt.Sprintf("Using config profile '%s' to determine if SBOM should be generated...", configProfile.ProfileName)) + return enableSca + } + userRequestedSpecificScans := len(scansToPerform) > 0 + if userRequestedSpecificScans { + if targetResult != nil { + log.Debug(fmt.Sprintf("Skipping SBOM generation for '%s' as requested by input...", targetResult.String())) + } else { + log.Debug("Skipping SBOM generation as requested by input...") + } + return false + } + // If we got here, we should generate the SBOM (all scans are requested) + return true } func logScanTargetsInfo(cmdResults *results.SecurityCommandResults) { + if len(cmdResults.Targets) == 0 { + log.Warn("No scan targets were detected. No scans will be performed.") + return + } // Print the scan targets if len(cmdResults.Targets) == 1 { - outLog := "Performing scans on " - if cmdResults.Targets[0].Technology != techutils.NoTech { - outLog += fmt.Sprintf("%s ", cmdResults.Targets[0].Technology.String()) - } - outLog += "project " - if cmdResults.Targets[0].Name != "" { - outLog += fmt.Sprintf("'%s' ", cmdResults.Targets[0].Name) - } else { - outLog += fmt.Sprintf("'%s' ", cmdResults.Targets[0].Target) - } - log.Info(outLog) + log.Info(fmt.Sprintf("Performing scans on project %s", cmdResults.Targets[0].String())) return } scanInfo, err := coreutils.GetJsonIndent(cmdResults.GetTargets()) if err != nil { return } - log.Info(fmt.Sprintf("Performing scans on %d targets:\n%s", len(cmdResults.Targets), scanInfo)) + log.Info(fmt.Sprintf("Performing scans on %d targets", len(cmdResults.Targets))) + log.Debug(scanInfo) } func getTargetResultsToCompare(cmdResults, resultsToCompare *results.SecurityCommandResults, targetResult *results.TargetResults) (targetResultsToCompare *results.TargetResults) { @@ -541,9 +562,7 @@ func getTargetResultsToCompare(cmdResults, resultsToCompare *results.SecurityCom return } targetResultsToCompare = results.SearchTargetResultsByRelativePath( - utils.GetRelativePath(targetResult.Target, cmdResults.GetCommonParentPath()), - targetResult.Technology, - resultsToCompare, + utils.GetRelativePath(targetResult.Target, cmdResults.GetCommonParentPath()), resultsToCompare, targetResult.Technologies..., ) // Let's check if the target results to compare are valid. // If the current target result is a new module, it will not have any previous target results to compare with. @@ -554,13 +573,154 @@ func getTargetResultsToCompare(cmdResults, resultsToCompare *results.SecurityCom } func detectScanTargets(cmdResults *results.SecurityCommandResults, params *AuditParams) { - for _, requestedDirectory := range params.workingDirs { + cwd, err := coreutils.GetWorkingDirectory() + if err != nil { + cmdResults.AddGeneralError(fmt.Errorf("failed to get working directory: %s", err.Error()), false) + return + } + // Create scan targets + if isNewFlow(params.bomGenerator) { + createScanTargetsFromConfigs(cmdResults, params, cwd) + } else { + // Old flow: + detectScaTargetsFromTechnologies(cmdResults, params, cwd) + matchCentralConfigModulesForOldFlow(cmdResults, params.GetConfigProfile()) + } +} + +// New flow: creates targets from config profile modules or working dirs input. +func createScanTargetsFromConfigs(cmdResults *results.SecurityCommandResults, params *AuditParams, cwd string) { + rootDir := params.rootDir + if rootDir == "" { + rootDir = cwd + } + configProfile := params.GetConfigProfile() + if configProfile == nil { + includeDirs := params.WorkingDirs() + msg := fmt.Sprintf("No config profile found. Creating single scan target from root directory: %s", rootDir) + if len(includeDirs) > 0 { + msg += fmt.Sprintf(" and working dirs: %s", strings.Join(includeDirs, ", ")) + } + log.Debug(msg) + if scanTarget := createScanTarget(rootDir, params.Exclusions(), includeDirs...); scanTarget != nil { + scanTarget.Technologies = detectTechnologiesInTarget(*scanTarget, params) + cmdResults.NewScanResults(*scanTarget) + } + return + } + log.Debug("Creating scan targets from config profile:", configProfile.ProfileName) + for _, module := range configProfile.Modules { + moduleRoot := rootDir + if module.PathFromRoot != "" && module.PathFromRoot != "." { + moduleRoot = filepath.Join(rootDir, module.PathFromRoot) + } + scanTarget := createScanTarget(moduleRoot, module.ExcludePatterns, module.IncludePatterns...) + if scanTarget == nil { + continue + } + scanTarget.Technologies = detectTechnologiesInTarget(*scanTarget, params) + scanTarget.CentralConfigModules = []xscServices.Module{module} + cmdResults.NewScanResults(*scanTarget) + } +} + +// Create a scan target from the given root directory, exclude patterns and optionally include patterns. +func createScanTarget(root string, exclude []string, includes ...string) *results.ScanTarget { + dirs := datastructures.MakeSet[string]() + // Validate include patterns + for _, includePattern := range includes { + // Check if the include pattern is a file or a directory. + if isDir, err := fileutils.IsDirExists(includePattern, false); err != nil { + log.Warn(fmt.Sprintf("Failed to check if '%s' is a directory: %s", includePattern, err.Error())) + continue + } else if isDir && !utils.IsPathExcluded(includePattern, exclude) { + includePath := includePattern + if !filepath.IsAbs(includePattern) { + includePath = filepath.Join(root, includePattern) + } + dirs.Add(includePath) + continue + } + // the pattern is not a directory, so we need to list the directories in the pattern. + log.Debug(fmt.Sprintf("The pattern '%s' is not a directory, listing directories in the pattern...", includePattern)) + includeDirs, err := utils.ListDirs(root, includePattern == root, true, true, utils.GetExcludePattern(exclude, utils.DefaultScaExcludePatterns, includePattern == root), includePattern) + if err != nil { + log.Warn(fmt.Sprintf("Failed to list directories for '%s': %s", includePattern, err.Error())) + continue + } + dirs.AddElements(includeDirs...) + } + include := dirs.ToSlice() + if utils.IsPathExcluded(root, exclude) { + if len(include) == 0 { + log.Warn(fmt.Sprintf("The working directory '%s' matches exclusion patterns %s. Skipping...", root, strings.Join(exclude, ", "))) + return nil + } + log.Debug(fmt.Sprintf("Root directory '%s' is excluded; creating scan target from %d explicit include path(s)", root, len(include))) + } + return &results.ScanTarget{Target: root, Include: include, Exclude: exclude} +} + +func detectTechnologiesInTarget(target results.ScanTarget, otherParams *AuditParams) (technologies []techutils.Technology) { + detectedTechnologies := datastructures.MakeSet[techutils.Technology]() + for _, included := range jas.GetRootsFromTarget(target) { + techToWorkingDirs, err := techutils.DetectTechnologiesDescriptors(included, included == target.Target, otherParams.Technologies(), nil, utils.GetExcludePattern(target.Exclude, utils.DefaultScaExcludePatterns, included == target.Target)) + if err != nil { + log.Warn(fmt.Sprintf("Couldn't detect technologies in '%s' directory: %s", included, err.Error())) + continue + } + for tech := range techToWorkingDirs { + detectedTechnologies.Add(tech) + } + } + return detectedTechnologies.ToSlice() +} + +func matchCentralConfigModulesForOldFlow(cmdResults *results.SecurityCommandResults, centralProfile *xscServices.ConfigProfile) { + if centralProfile == nil { + return + } + if len(centralProfile.Modules) < 1 { + // Verify Modules are not nil and contain at least one modules + cmdResults.AddGeneralError(fmt.Errorf("config profile %s has no modules. A config profile must contain at least one modules", centralProfile.ProfileName), false) + return + } + log.Debug(fmt.Sprintf("Assigning all (%d) config profile module(s) from '%s' to each of the %d scan target(s)", len(centralProfile.Modules), centralProfile.ProfileName, len(cmdResults.Targets))) + for _, targetResult := range cmdResults.Targets { + // TODO: support matching multiple config modules to the scan targets + // currently only supported one config module for all targets to configure in the UI + // PathFromRoot is always '.' + targetResult.CentralConfigModules = centralProfile.Modules + } +} + +// Old flow: creates targets from technologies detected in the working directories. +func detectScaTargetsFromTechnologies(cmdResults *results.SecurityCommandResults, params *AuditParams, cwd string) { + exclusions := params.Exclusions() + if configProfile := params.GetConfigProfile(); configProfile != nil { + // TODO: support matching multiple config modules to the scan targets + exclusions = append(exclusions, configProfile.Modules[0].ExcludePatterns...) + exclusions = append(exclusions, configProfile.Modules[0].ScanConfig.ScaScannerConfig.ExcludePatterns...) + } + potentialScanTargets := []string{cwd} + if len(params.workingDirs) > 0 { + potentialScanTargets = params.workingDirs + } + dirsToDetect := []string{} + for _, requestedDirectory := range potentialScanTargets { if !fileutils.IsPathExists(requestedDirectory, false) { log.Warn("The working directory", requestedDirectory, "doesn't exist. Skipping SCA scan...") continue } + if isExcluded := utils.IsPathExcluded(requestedDirectory, exclusions); isExcluded { + log.Warn(fmt.Sprintf("The working directory '%s' matches exclusion patterns %s. Skipping...", requestedDirectory, strings.Join(exclusions, ", "))) + continue + } + dirsToDetect = append(dirsToDetect, requestedDirectory) + } + for _, requestedDirectory := range dirsToDetect { // Detect descriptors and technologies in the requested directory. - techToWorkingDirs, err := techutils.DetectTechnologiesDescriptors(requestedDirectory, params.IsRecursiveScan(), params.Technologies(), getRequestedDescriptors(params), technologies.GetExcludePattern(params.GetConfigProfile(), params.IsRecursiveScan(), params.Exclusions()...)) + techToWorkingDirs, err := techutils.DetectTechnologiesDescriptors(requestedDirectory, params.IsRecursiveScan(), params.Technologies(), getRequestedDescriptors(params), utils.GetExcludePattern(exclusions, utils.DefaultScaExcludePatterns, params.IsRecursiveScan())) if err != nil { log.Warn("Couldn't detect technologies in", requestedDirectory, "directory.", err.Error()) continue @@ -575,11 +735,21 @@ func detectScanTargets(cmdResults *results.SecurityCommandResults, params *Audit // No technology was detected, add scan without descriptors. (so no sca scan will be performed and set at target level) if len(workingDirs) == 0 { // Requested technology (from params) descriptors/indicators were not found or recursive scan with NoTech value, add scan without descriptors. - cmdResults.NewScanResults(results.ScanTarget{Target: requestedDirectory, Technology: tech}) + scanTarget := createScanTarget(requestedDirectory, exclusions) + if scanTarget == nil { + continue + } + scanTarget.Technologies = []techutils.Technology{tech} + cmdResults.NewScanResults(*scanTarget) } for workingDir, descriptors := range workingDirs { // Add scan for each detected working directory. - targetResults := cmdResults.NewScanResults(results.ScanTarget{Target: workingDir, Technology: tech}) + scanTarget := createScanTarget(workingDir, exclusions) + if scanTarget == nil { + continue + } + scanTarget.Technologies = []techutils.Technology{tech} + targetResults := cmdResults.NewScanResults(*scanTarget) if tech != techutils.NoTech { targetResults.SetDescriptors(descriptors...) } @@ -587,8 +757,22 @@ func detectScanTargets(cmdResults *results.SecurityCommandResults, params *Audit } } // If no scan targets were detected, we should still proceed with the scans. - if len(params.workingDirs) == 1 && len(cmdResults.Targets) == 0 { - cmdResults.NewScanResults(results.ScanTarget{Target: params.workingDirs[0]}) + if len(dirsToDetect) == 1 && params.IsRecursiveScan() && len(cmdResults.Targets) == 0 { + if scanTarget := createScanTarget(dirsToDetect[0], exclusions); scanTarget != nil { + cmdResults.NewScanResults(*scanTarget) + } + } + // Load deprecated apps config information for all targets + if params.DeprecatedAppsConfig() == nil { + jfrogAppsConfig, err := jas.CreateJFrogAppsConfig(cmdResults.GetTargetsPaths()) + if err != nil { + cmdResults.AddGeneralError(fmt.Errorf("failed to create JFrogAppsConfig: %s", err.Error()), false) + return + } + params.SetDeprecatedAppsConfig(jfrogAppsConfig) + } + for _, targetResult := range cmdResults.Targets { + targetResult.DeprecatedAppsConfigModule = jas.GetModule(targetResult.Target, params.DeprecatedAppsConfig()) } } @@ -608,16 +792,13 @@ func runParallelAuditScans(cmdResults *results.SecurityCommandResults, auditPara auditParams.Progress().SetHeadlineMsg("Scanning for issues") } // TODO: remove "isNewFlow" once the old flow is fully deprecated. - isNewFlow := true - if _, ok := auditParams.scaScanStrategy.(*scanGraphStrategy.ScanGraphStrategy); ok { - isNewFlow = false - } + isNewFlow := isNewFlow(auditParams.bomGenerator) // Add the scans to the parallel runner if jasScanner, generalJasScanErr = addJasScansToRunner(auditParallelRunner, auditParams, cmdResults, isNewFlow); generalJasScanErr != nil { - cmdResults.AddGeneralError(fmt.Errorf("error has occurred during JAS scan process. JAS scan is skipped for the following directories: %s\n%s", strings.Join(cmdResults.GetTargetsPaths(), ","), generalJasScanErr.Error()), auditParams.AllowPartialResults()) + cmdResults.AddGeneralError(fmt.Errorf("error has occurred during JAS scan process. JAS scan is skipped for the following directories: %s\n%s", strings.Join(cmdResults.GetTargetsPaths(), ","), generalJasScanErr.Error()), cmdResults.AllowPartialResults) } if generalScaScanError := addScaScansToRunner(auditParallelRunner, auditParams, cmdResults, isNewFlow); generalScaScanError != nil { - cmdResults.AddGeneralError(fmt.Errorf("error has occurred during SCA scan process. SCA scan is skipped for the following directories: %s\n%s", strings.Join(cmdResults.GetTargetsPaths(), ","), generalScaScanError.Error()), auditParams.AllowPartialResults()) + cmdResults.AddGeneralError(fmt.Errorf("error has occurred during SCA scan process. SCA scan is skipped for the following directories: %s\n%s", strings.Join(cmdResults.GetTargetsPaths(), ","), generalScaScanError.Error()), cmdResults.AllowPartialResults) } // Start the parallel runner to run the scans. auditParallelRunner.OnScanEnd(func() { @@ -645,7 +826,7 @@ func addScaScansToRunner(auditParallelRunner *utils.SecurityParallelRunner, audi TargetCount: len(scanResults.Targets), ScansToPerform: auditParams.ScansToPerform(), ConfigProfile: auditParams.GetConfigProfile(), - AllowPartialResults: auditParams.AllowPartialResults(), + AllowPartialResults: scanResults.AllowPartialResults, ResultsOutputDir: auditParams.scanResultsOutputDir, Runner: auditParallelRunner, // TODO: remove this field once the new flow is fully implemented. @@ -662,7 +843,7 @@ func addJasScansToRunner(auditParallelRunner *utils.SecurityParallelRunner, audi log.Info("Advanced Security is not enabled on this system, so Advanced Security scans were skipped...") return } - if !utils.IsJASRequested(scanResults.CmdType, auditParams.ScansToPerform()...) { + if !scanResults.IsJASRequested(auditParams.ScansToPerform()...) { log.Debug("Advanced Security scans were not initiated, so Advanced Security scans were skipped...") return } @@ -674,7 +855,6 @@ func addJasScansToRunner(auditParallelRunner *utils.SecurityParallelRunner, audi auditParallelRunner.ResultsMu.Lock() scannerOptions := []jas.JasScannerOption{ jas.WithEnvVars( - scanResults.SecretValidation, jas.GetDiffScanTypeValue(auditParams.diffMode, auditParams.resultsToCompare), jas.GetAnalyzerManagerXscEnvVars( isNewFlow, @@ -691,7 +871,6 @@ func addJasScansToRunner(auditParallelRunner *utils.SecurityParallelRunner, audi jas.WithResultsToCompare(auditParams.resultsToCompare), } jasScanner, err = jas.NewJasScanner(serverDetails, scannerOptions...) - jas.UpdateJasScannerWithExcludePatternsFromProfile(jasScanner, auditParams.GetConfigProfile()) auditParallelRunner.ResultsMu.Unlock() if err != nil { @@ -702,8 +881,8 @@ func addJasScansToRunner(auditParallelRunner *utils.SecurityParallelRunner, audi return } auditParallelRunner.JasWg.Add(1) - if _, jasErr := auditParallelRunner.Runner.AddTaskWithError(createJasScansTask(auditParallelRunner, scanResults, serverDetails, auditParams, jasScanner), func(taskErr error) { - scanResults.AddGeneralError(fmt.Errorf("failed while adding JAS scan tasks: %s", taskErr.Error()), auditParams.AllowPartialResults()) + if _, jasErr := auditParallelRunner.Runner.AddTaskWithError(createJasScansTask(auditParallelRunner, scanResults, serverDetails, auditParams, jasScanner, isNewFlow), func(taskErr error) { + scanResults.AddGeneralError(fmt.Errorf("failed while adding JAS scan tasks: %s", taskErr.Error()), scanResults.AllowPartialResults) }); jasErr != nil { generalError = fmt.Errorf("failed to create JAS task: %s", jasErr.Error()) } @@ -711,14 +890,18 @@ func addJasScansToRunner(auditParallelRunner *utils.SecurityParallelRunner, audi } func createJasScansTask(auditParallelRunner *utils.SecurityParallelRunner, scanResults *results.SecurityCommandResults, - serverDetails *config.ServerDetails, auditParams *AuditParams, scanner *jas.JasScanner) parallel.TaskFunc { + serverDetails *config.ServerDetails, auditParams *AuditParams, scanner *jas.JasScanner, isNewFlow bool) parallel.TaskFunc { return func(threadId int) (generalError error) { defer func() { auditParallelRunner.JasWg.Done() }() // First download the analyzer manager if needed if auditParams.customAnalyzerManagerBinaryPath == "" { - if generalError = jas.DownloadAnalyzerManagerIfNeeded(threadId); generalError != nil { + centralConfigDownloadPath := "" + if auditParams.configProfile != nil { + centralConfigDownloadPath = auditParams.configProfile.GeneralConfig.ScannersDownloadPath + } + if generalError = jas.DownloadAnalyzerManagerIfNeeded(centralConfigDownloadPath, serverDetails, threadId); generalError != nil { return fmt.Errorf("failed to download analyzer manager: %s", generalError.Error()) } if scanner.AnalyzerManager.AnalyzerManagerFullPath, generalError = jas.GetAnalyzerManagerExecutable(); generalError != nil { @@ -731,24 +914,23 @@ func createJasScansTask(auditParallelRunner *utils.SecurityParallelRunner, scanR log.Debug(clientutils.GetLogMsgPrefix(threadId, false) + fmt.Sprintf("Using analyzer manager executable at: %s", scanner.AnalyzerManager.AnalyzerManagerFullPath)) // Run JAS scanners for each scan target for _, targetResult := range scanResults.Targets { - if targetResult.AppsConfigModule == nil { - _ = targetResult.AddTargetError(fmt.Errorf("can't find module for path %s", targetResult.Target), auditParams.AllowPartialResults()) + if !isNewFlow && targetResult.DeprecatedAppsConfigModule == nil { + _ = targetResult.AddTargetError(fmt.Errorf("can't find module for path %s", targetResult.Target), scanResults.AllowPartialResults) continue } - appsConfigModule := *targetResult.AppsConfigModule params := runner.JasRunnerParams{ Runner: auditParallelRunner, ServerDetails: serverDetails, Scanner: scanner, - Module: appsConfigModule, ConfigProfile: auditParams.GetConfigProfile(), ScansToPerform: auditParams.ScansToPerform(), - SourceResultsToCompare: scanner.GetResultsToCompareByRelativePath(utils.GetRelativePath(targetResult.Target, scanResults.GetCommonParentPath()), targetResult.Technology), + SourceResultsToCompare: scanner.GetResultsToCompareByRelativePath(utils.GetRelativePath(targetResult.Target, scanResults.GetCommonParentPath()), targetResult.Technologies...), SecretsScanType: secrets.SecretsScannerType, + SecretValidation: scanResults.SecretValidation && targetResult.ShouldValidateSecrets(slices.Contains(auditParams.ScansToPerform(), utils.SecretTokenValidationScan)), CvesProvider: func() (directCves []string, indirectCves []string) { if len(targetResult.GetScaScansXrayResults()) > 0 { // TODO: remove this once the new SCA flow with cdx is fully implemented. - return results.ExtractCvesFromScanResponse(targetResult.GetScaScansXrayResults(), results.GetTargetDirectDependencies(targetResult, auditParams.ShouldGetFlatTreeForApplicableScan(targetResult.Technology), true)) + return results.ExtractCvesFromScanResponse(targetResult.GetScaScansXrayResults(), results.GetTargetDirectDependencies(targetResult, auditParams.ShouldGetFlatTreeForApplicableScan(targetResult.ScanTarget), true)) } else if targetResult.ScaResults != nil && targetResult.ScaResults.Sbom != nil { return results.ExtractCdxDependenciesCves(targetResult.ScaResults.Sbom) } @@ -763,10 +945,10 @@ func createJasScansTask(auditParallelRunner *utils.SecurityParallelRunner, scanR ScanResults: targetResult, TargetCount: len(scanResults.Targets), TargetOutputDir: auditParams.scanResultsOutputDir, - AllowPartialResults: auditParams.AllowPartialResults(), + AllowPartialResults: scanResults.AllowPartialResults, } if generalError = runner.AddJasScannersTasks(params); generalError != nil { - _ = targetResult.AddTargetError(fmt.Errorf("failed to add JAS scan tasks: %s", generalError.Error()), auditParams.AllowPartialResults()) + _ = targetResult.AddTargetError(fmt.Errorf("failed to add JAS scan tasks: %s", generalError.Error()), scanResults.AllowPartialResults) // We assign nil to 'generalError' after handling it to prevent it to propagate further, so it will not be captured twice - once here, and once in the error handling function of createJasScansTasks generalError = nil } @@ -822,7 +1004,7 @@ func processScanResults(params *AuditParams, cmdResults *results.SecurityCommand rtResultRepository = fmt.Sprintf("%s-%s", params.resultsContext.ProjectKey, rtResultRepository) } if err = fetchViolations(uploadPath, cmdResults, params, rtResultRepository); err != nil { - cmdResults.AddGeneralError(fmt.Errorf("failed to get violations: %s", err.Error()), params.AllowPartialResults()) + cmdResults.AddGeneralError(fmt.Errorf("failed to get violations: %s", err.Error()), cmdResults.AllowPartialResults) } } return cmdResults diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go index 40ecf4012..a0a2900de 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/jfrog/jfrog-cli-security/policy/local" + "github.com/jfrog/jfrog-cli-security/sca/bom/xrayplugin" "github.com/jfrog/jfrog-cli-security/tests/validations" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/results" @@ -58,14 +59,15 @@ func TestDetectScansToPerform(t *testing.T) { { // We requested specific technologies, Nuget is not in the list but we want to run JAS on it ScanTarget: results.ScanTarget{ - Target: filepath.Join(dir, "Nuget"), + Target: filepath.Join(dir, "Nuget"), + Technologies: []techutils.Technology{techutils.NoTech}, }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, }, { ScanTarget: results.ScanTarget{ - Technology: techutils.Go, - Target: filepath.Join(dir, "dir", "go"), + Technologies: []techutils.Technology{techutils.Go}, + Target: filepath.Join(dir, "dir", "go"), }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, ScaResults: &results.ScaScanResults{ @@ -74,8 +76,8 @@ func TestDetectScansToPerform(t *testing.T) { }, { ScanTarget: results.ScanTarget{ - Technology: techutils.Maven, - Target: filepath.Join(dir, "dir", "maven"), + Technologies: []techutils.Technology{techutils.Maven}, + Target: filepath.Join(dir, "dir", "maven"), }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, ScaResults: &results.ScaScanResults{ @@ -88,8 +90,8 @@ func TestDetectScansToPerform(t *testing.T) { }, { ScanTarget: results.ScanTarget{ - Technology: techutils.Npm, - Target: filepath.Join(dir, "dir", "npm"), + Technologies: []techutils.Technology{techutils.Npm}, + Target: filepath.Join(dir, "dir", "npm"), }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, ScaResults: &results.ScaScanResults{ @@ -99,7 +101,8 @@ func TestDetectScansToPerform(t *testing.T) { { // We requested specific technologies, yarn is not in the list but we want to run JAS on it ScanTarget: results.ScanTarget{ - Target: filepath.Join(dir, "yarn"), + Target: filepath.Join(dir, "yarn"), + Technologies: []techutils.Technology{techutils.NoTech}, }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, }, @@ -116,8 +119,8 @@ func TestDetectScansToPerform(t *testing.T) { expected: []*results.TargetResults{ { ScanTarget: results.ScanTarget{ - Technology: techutils.Nuget, - Target: filepath.Join(dir, "Nuget"), + Technologies: []techutils.Technology{techutils.Nuget}, + Target: filepath.Join(dir, "Nuget"), }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, ScaResults: &results.ScaScanResults{ @@ -126,8 +129,8 @@ func TestDetectScansToPerform(t *testing.T) { }, { ScanTarget: results.ScanTarget{ - Technology: techutils.Go, - Target: filepath.Join(dir, "dir", "go"), + Technologies: []techutils.Technology{techutils.Go}, + Target: filepath.Join(dir, "dir", "go"), }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, ScaResults: &results.ScaScanResults{ @@ -136,8 +139,8 @@ func TestDetectScansToPerform(t *testing.T) { }, { ScanTarget: results.ScanTarget{ - Technology: techutils.Maven, - Target: filepath.Join(dir, "dir", "maven"), + Technologies: []techutils.Technology{techutils.Maven}, + Target: filepath.Join(dir, "dir", "maven"), }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, ScaResults: &results.ScaScanResults{ @@ -150,8 +153,8 @@ func TestDetectScansToPerform(t *testing.T) { }, { ScanTarget: results.ScanTarget{ - Technology: techutils.Npm, - Target: filepath.Join(dir, "dir", "npm"), + Technologies: []techutils.Technology{techutils.Npm}, + Target: filepath.Join(dir, "dir", "npm"), }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, ScaResults: &results.ScaScanResults{ @@ -160,8 +163,8 @@ func TestDetectScansToPerform(t *testing.T) { }, { ScanTarget: results.ScanTarget{ - Technology: techutils.Yarn, - Target: filepath.Join(dir, "yarn"), + Technologies: []techutils.Technology{techutils.Yarn}, + Target: filepath.Join(dir, "yarn"), }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, ScaResults: &results.ScaScanResults{ @@ -170,8 +173,8 @@ func TestDetectScansToPerform(t *testing.T) { }, { ScanTarget: results.ScanTarget{ - Technology: techutils.Pip, - Target: filepath.Join(dir, "yarn", "Pip"), + Technologies: []techutils.Technology{techutils.Pip}, + Target: filepath.Join(dir, "yarn", "Pip"), }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, ScaResults: &results.ScaScanResults{ @@ -180,8 +183,8 @@ func TestDetectScansToPerform(t *testing.T) { }, { ScanTarget: results.ScanTarget{ - Technology: techutils.Pipenv, - Target: filepath.Join(dir, "yarn", "Pipenv"), + Technologies: []techutils.Technology{techutils.Pipenv}, + Target: filepath.Join(dir, "yarn", "Pipenv"), }, JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, ScaResults: &results.ScaScanResults{ @@ -190,10 +193,245 @@ func TestDetectScansToPerform(t *testing.T) { }, }, }, + { + name: "Non-recursive scan on directory with descriptor at top level", + wd: dir, + params: func() *AuditParams { + param := NewAuditParams().SetWorkingDirs([]string{filepath.Join(dir, "dir", "npm")}) + param.SetIsRecursiveScan(false) + return param + }, + expected: []*results.TargetResults{ + { + ScanTarget: results.ScanTarget{ + Technologies: []techutils.Technology{techutils.Npm}, + Target: filepath.Join(dir, "dir", "npm"), + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "dir", "npm", "package.json")}, + }, + }, + }, + }, + { + name: "Single technology (npm only)", + wd: dir, + params: func() *AuditParams { + param := NewAuditParams().SetWorkingDirs([]string{dir}) + param.SetTechnologies([]string{"npm"}).SetIsRecursiveScan(true) + return param + }, + expected: []*results.TargetResults{ + { + ScanTarget: results.ScanTarget{ + Technologies: []techutils.Technology{techutils.Npm}, + Target: filepath.Join(dir, "dir", "npm"), + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "dir", "npm", "package.json")}, + }, + }, + { + // Requested tech npm had no other descriptors at root; add JAS-only target for requested directory + ScanTarget: results.ScanTarget{ + Target: dir, + Technologies: []techutils.Technology{techutils.NoTech}, + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + }, + }, + }, + { + name: "Multiple working dirs (subset - dir and yarn only, no Nuget)", + wd: dir, + params: func() *AuditParams { + param := NewAuditParams().SetWorkingDirs([]string{filepath.Join(dir, "dir"), filepath.Join(dir, "yarn")}) + param.SetIsRecursiveScan(true) + return param + }, + expected: []*results.TargetResults{ + { + ScanTarget: results.ScanTarget{ + Technologies: []techutils.Technology{techutils.Go}, + Target: filepath.Join(dir, "dir", "go"), + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "dir", "go", "go.mod")}, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technologies: []techutils.Technology{techutils.Maven}, + Target: filepath.Join(dir, "dir", "maven"), + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{ + filepath.Join(dir, "dir", "maven", "maven-sub", "pom.xml"), + filepath.Join(dir, "dir", "maven", "maven-sub2", "pom.xml"), + filepath.Join(dir, "dir", "maven", "pom.xml"), + }, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technologies: []techutils.Technology{techutils.Npm}, + Target: filepath.Join(dir, "dir", "npm"), + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "dir", "npm", "package.json")}, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technologies: []techutils.Technology{techutils.Pip}, + Target: filepath.Join(dir, "yarn", "Pip"), + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "yarn", "Pip", "requirements.txt")}, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technologies: []techutils.Technology{techutils.Pipenv}, + Target: filepath.Join(dir, "yarn", "Pipenv"), + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "yarn", "Pipenv", "Pipfile")}, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technologies: []techutils.Technology{techutils.Yarn}, + Target: filepath.Join(dir, "yarn"), + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "yarn", "package.json")}, + }, + }, + }, + }, + { + name: "Single technology (maven only)", + wd: dir, + params: func() *AuditParams { + param := NewAuditParams().SetWorkingDirs([]string{filepath.Join(dir, "dir", "maven")}) + param.SetIsRecursiveScan(true) + return param + }, + expected: []*results.TargetResults{ + { + ScanTarget: results.ScanTarget{ + Technologies: []techutils.Technology{techutils.Maven}, + Target: filepath.Join(dir, "dir", "maven"), + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{ + filepath.Join(dir, "dir", "maven", "maven-sub", "pom.xml"), + filepath.Join(dir, "dir", "maven", "maven-sub2", "pom.xml"), + filepath.Join(dir, "dir", "maven", "pom.xml"), + }, + }, + }, + }, + }, + { + name: "Non-recursive on go directory", + wd: dir, + params: func() *AuditParams { + param := NewAuditParams().SetWorkingDirs([]string{filepath.Join(dir, "dir", "go")}) + param.SetIsRecursiveScan(false) + return param + }, + expected: []*results.TargetResults{ + { + ScanTarget: results.ScanTarget{ + Technologies: []techutils.Technology{techutils.Go}, + Target: filepath.Join(dir, "dir", "go"), + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "dir", "go", "go.mod")}, + }, + }, + }, + }, + { + name: "Single target with one working dir (npm)", + wd: dir, + params: func() *AuditParams { + param := NewAuditParams(). + SetWorkingDirs([]string{filepath.Join(dir, "dir", "npm")}). + SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + param.SetIsRecursiveScan(false) + return param + }, + expected: []*results.TargetResults{ + { + ScanTarget: results.ScanTarget{ + Target: dir, + Include: []string{filepath.Join(dir, "dir", "npm")}, + Technologies: []techutils.Technology{techutils.Npm}, + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + }, + }, + }, + { + name: "Single target with one working dir (maven)", + wd: dir, + params: func() *AuditParams { + param := NewAuditParams().SetWorkingDirs([]string{filepath.Join(dir, "dir", "maven")}) + return param + }, + expected: []*results.TargetResults{ + { + ScanTarget: results.ScanTarget{ + Target: filepath.Join(dir, "dir", "maven"), + Technologies: []techutils.Technology{techutils.Maven}, + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "dir", "maven", "pom.xml")}, + }, + }, + }, + }, + { + name: "Single target root directory with one include directory (maven) - new flow", + wd: dir, + params: func() *AuditParams { + param := NewAuditParams(). + SetWorkingDirs([]string{filepath.Join(dir, "dir", "maven")}). + SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + return param + }, + expected: []*results.TargetResults{ + { + ScanTarget: results.ScanTarget{ + Target: dir, + Include: []string{filepath.Join(dir, "dir", "maven")}, + Technologies: []techutils.Technology{techutils.Maven}, + }, + JasResults: &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}, JasViolations: results.JasScanResults{}}, + }, + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { + if test.wd != "" { + defer securityTestUtils.ChangeWDWithCallback(t, test.wd)() + } results := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true).SetSecretValidation(true) detectScanTargets(results, test.params()) if assert.Len(t, results.Targets, len(test.expected)) { @@ -210,6 +448,13 @@ func TestDetectScansToPerform(t *testing.T) { if test.expected[i].ScaResults != nil { sort.Strings(test.expected[i].ScaResults.Descriptors) } + // Normalize for comparison: DeprecatedAppsConfigModule varies by working dir and is not under test + results.Targets[i].DeprecatedAppsConfigModule = nil + // Normalize single-target expected to actual cwd (path can differ e.g. /var vs /private/var on macOS) + if len(results.Targets) == 1 && len(results.Targets[i].Include) > 0 { + test.expected[i].Target = results.Targets[i].Target + test.expected[i].Include = results.Targets[i].Include + } } } assert.ElementsMatch(t, test.expected, results.Targets) @@ -219,30 +464,96 @@ func TestDetectScansToPerform(t *testing.T) { cleanUp() } +func TestDetectScanTargetsSkipsCliExcludedCwdSingleTarget(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + + excludedCwd := filepath.Join(baseDir, "parent_only_excluded_cli_audit") + assert.NoError(t, os.MkdirAll(excludedCwd, 0o755)) + defer securityTestUtils.ChangeWDWithCallback(t, excludedCwd)() + + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true).SetSecretValidation(true) + params := NewAuditParams() + params.SetExclusions([]string{"*parent_only_excluded_cli_audit*"}) + + detectScanTargets(cmdRes, params) + assert.Empty(t, cmdRes.Targets) +} + +func TestDetectScanTargetsSkipsCliExcludedExplicitWorkingDir(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + + excludedOnly := filepath.Join(baseDir, "cli_exclude_workdir_only_audit") + assert.NoError(t, os.MkdirAll(excludedOnly, 0o755)) + createEmptyFile(t, filepath.Join(excludedOnly, "package.json")) + + defer securityTestUtils.ChangeWDWithCallback(t, baseDir)() + + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true).SetSecretValidation(true) + params := NewAuditParams() + params.SetWorkingDirs([]string{excludedOnly}) + params.SetIsRecursiveScan(false) + params.SetExclusions([]string{"*cli_exclude_workdir_only_audit*"}) + + detectScanTargets(cmdRes, params) + assert.Empty(t, cmdRes.Targets) +} + +func TestDetectScanTargetsNewFlowCliExcludedCwdWithNonExcludedInclude(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + + excludedCwd := filepath.Join(baseDir, "parent_only_excluded_for_agg") + siblingNpm := filepath.Join(baseDir, "sibling_npm_for_agg_test") + assert.NoError(t, os.MkdirAll(excludedCwd, 0o755)) + assert.NoError(t, os.MkdirAll(siblingNpm, 0o755)) + createEmptyFile(t, filepath.Join(siblingNpm, "package.json")) + + defer securityTestUtils.ChangeWDWithCallback(t, excludedCwd)() + + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true).SetSecretValidation(true) + params := NewAuditParams() + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + params.SetWorkingDirs([]string{siblingNpm}) + params.SetExclusions([]string{"*parent_only_excluded_for_agg*"}) + + detectScanTargets(cmdRes, params) + assert.Len(t, cmdRes.Targets, 1) + tr := cmdRes.Targets[0] + tr.DeprecatedAppsConfigModule = nil + assert.Len(t, tr.Include, 1) + assert.Equal(t, siblingNpm, tr.Include[0]) + hasNpm := false + for _, tech := range tr.Technologies { + if tech == techutils.Npm { + hasNpm = true + } + } + assert.True(t, hasNpm, "expected Npm among detected technologies") +} + func TestShouldGenerateSbom(t *testing.T) { - configProfileWithSca := services.ConfigProfile{ - Modules: []services.Module{{ - ScanConfig: services.ScanConfig{ - ScaScannerConfig: services.ScaScannerConfig{ - EnableScaScan: true, - }, + configProfileModulesWithSca := []services.Module{{ + ScanConfig: services.ScanConfig{ + ScaScannerConfig: services.ScaScannerConfig{ + EnableScaScan: true, }, - }}, - } - configProfileWithoutSca := services.ConfigProfile{ - Modules: []services.Module{{ - ScanConfig: services.ScanConfig{ - ScaScannerConfig: services.ScaScannerConfig{ - EnableScaScan: false, - }, + }, + }} + configProfileModulesWithoutSca := []services.Module{{ + ScanConfig: services.ScanConfig{ + ScaScannerConfig: services.ScaScannerConfig{ + EnableScaScan: false, }, - }}, - } + }, + }} testCases := []struct { - name string - params *AuditParams - expectSbom bool + name string + params *AuditParams + scanResults *results.TargetResults + expectSbom bool }{ { name: "include sbom requested explicitly", @@ -294,26 +605,26 @@ func TestShouldGenerateSbom(t *testing.T) { params: func() *AuditParams { params := NewAuditParams().SetResultsContext(results.ResultContext{}) params.SetScansToPerform([]utils.SubScanType{utils.SastScan}) - params.SetConfigProfile(&configProfileWithSca) return params }(), - expectSbom: true, + scanResults: &results.TargetResults{ScanTarget: results.ScanTarget{CentralConfigModules: configProfileModulesWithSca}}, + expectSbom: true, }, { name: "non sca scans with sca disabled in config profile", params: func() *AuditParams { params := NewAuditParams().SetResultsContext(results.ResultContext{}) params.SetScansToPerform([]utils.SubScanType{utils.SastScan}) - params.SetConfigProfile(&configProfileWithoutSca) return params }(), - expectSbom: false, + scanResults: &results.TargetResults{ScanTarget: results.ScanTarget{CentralConfigModules: configProfileModulesWithoutSca}}, + expectSbom: false, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - assert.Equal(t, testCase.expectSbom, shouldGenerateSbom(testCase.params)) + assert.Equal(t, testCase.expectSbom, shouldGenerateSbom(testCase.scanResults, testCase.params)) }) } } @@ -1052,8 +1363,8 @@ func TestAudit_DiffScanFlow(t *testing.T) { Targets: []*results.TargetResults{ { ScanTarget: results.ScanTarget{ - Target: tempProjectPath, - Technology: techutils.Pip, + Target: tempProjectPath, + Technologies: []techutils.Technology{techutils.Pip}, }, ScaResults: &results.ScaScanResults{ Sbom: &cyclonedx.BOM{ diff --git a/commands/audit/auditbasicparams.go b/commands/audit/auditbasicparams.go index c20efa983..318dbe53f 100644 --- a/commands/audit/auditbasicparams.go +++ b/commands/audit/auditbasicparams.go @@ -50,7 +50,6 @@ type AuditParamsInterface interface { SetIsRecursiveScan(isRecursiveScan bool) *AuditBasicParams IsRecursiveScan() bool SkipAutoInstall() bool - AllowPartialResults() bool GetXrayVersion() string GetConfigProfile() *xscservices.ConfigProfile SolutionFilePath() string @@ -328,8 +327,8 @@ func (abp *AuditBasicParams) SkipAutoInstall() bool { return abp.skipAutoInstall } -func (abp *AuditBasicParams) AllowPartialResults() bool { - return abp.allowPartialResults +func (abp *AuditBasicParams) CalculatedAllowPartialResults() bool { + return (abp.configProfile != nil && !abp.configProfile.GeneralConfig.FailUponAnyScannerError) || abp.allowPartialResults } func (abp *AuditBasicParams) SetXrayVersion(xrayVersion string) *AuditBasicParams { diff --git a/commands/audit/auditparams.go b/commands/audit/auditparams.go index fdaf76293..99b7a18aa 100644 --- a/commands/audit/auditparams.go +++ b/commands/audit/auditparams.go @@ -3,6 +3,7 @@ package audit import ( "time" + jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" "github.com/jfrog/jfrog-client-go/xray/services" xscServices "github.com/jfrog/jfrog-client-go/xsc/services" @@ -18,10 +19,12 @@ import ( ) type AuditParams struct { + // Where to scan + appsConfig *jfrogappsconfig.JFrogAppsConfig + workingDirs []string // Common params to all scan routines resultsContext results.ResultContext gitContext *xscServices.XscGitInfoContext - workingDirs []string rootDir string installFunc func(tech string) error fixableOnly bool @@ -222,8 +225,8 @@ func (params *AuditParams) ToBuildInfoBomGenParams() (bomParams technologies.Bui bomParams = technologies.BuildInfoBomGeneratorParams{ XrayVersion: params.GetXrayVersion(), Progress: params.Progress(), - ExclusionPattern: technologies.GetExcludePattern(params.GetConfigProfile(), params.IsRecursiveScan(), params.Exclusions()...), - AllowPartialResults: params.AllowPartialResults(), + ExclusionPattern: technologies.GetScaExcludePattern(params.GetConfigProfile(), params.IsRecursiveScan(), params.Exclusions()...), + AllowPartialResults: params.CalculatedAllowPartialResults(), // Artifactory repository info ServerDetails: serverDetails, DependenciesRepository: params.DepsRepo(), @@ -273,6 +276,15 @@ func (params *AuditParams) SetSastChangedFilesMode(sastChangedFilesMode bool) *A return params } +func (params *AuditParams) SetDeprecatedAppsConfig(appsConfig *jfrogappsconfig.JFrogAppsConfig) *AuditParams { + params.appsConfig = appsConfig + return params +} + +func (params *AuditParams) DeprecatedAppsConfig() *jfrogappsconfig.JFrogAppsConfig { + return params.appsConfig +} + func (params *AuditParams) SastChangedFilesMode() bool { return params.sastChangedFilesMode } @@ -299,15 +311,14 @@ func (params *AuditParams) DiffMode() bool { // Our solution for this case is to send all dependencies to the CA scanner. // When thirdPartyApplicabilityScan is true, use flatten graph to include all the dependencies in applicability scanning. // Only npm is supported for this flag. -func (params *AuditParams) ShouldGetFlatTreeForApplicableScan(tech techutils.Technology) bool { +func (params *AuditParams) ShouldGetFlatTreeForApplicableScan(target results.ScanTarget) bool { if params.bomGenerator == nil { return false } - // Check if bomGenerator is BuildInfo type, if not, return false if _, success := params.bomGenerator.(*buildinfo.BuildInfoBomGenerator); !success { return false } - return tech == techutils.Pip || (params.thirdPartyApplicabilityScan && tech == techutils.Npm) + return target.HasTechnology(techutils.Pip) || (params.thirdPartyApplicabilityScan && target.HasTechnology(techutils.Npm)) } func (params *AuditParams) SetViolationGenerator(violationGenerator policy.PolicyHandler) *AuditParams { @@ -345,3 +356,8 @@ func (params *AuditParams) SetRtResultRepository(rtResultRepository string) *Aud func (params *AuditParams) RtResultRepository() string { return params.rtResultRepository } + +func (params *AuditParams) SetIncludeSbom(include bool) *AuditParams { + params.resultsContext.IncludeSbom = include + return params +} diff --git a/commands/curation/curationaudit.go b/commands/curation/curationaudit.go index d12fcc5b3..e37ff7cee 100644 --- a/commands/curation/curationaudit.go +++ b/commands/curation/curationaudit.go @@ -439,7 +439,7 @@ func (ca *CurationAuditCommand) getBuildInfoParamsByTech() (technologies.BuildIn serverDetails, err := ca.ServerDetails() return technologies.BuildInfoBomGeneratorParams{ XrayVersion: ca.GetXrayVersion(), - ExclusionPattern: technologies.GetExcludePattern(ca.GetConfigProfile(), ca.IsRecursiveScan(), ca.Exclusions()...), + ExclusionPattern: technologies.GetScaExcludePattern(ca.GetConfigProfile(), ca.IsRecursiveScan(), ca.Exclusions()...), Progress: ca.Progress(), // Artifactory Repository params ServerDetails: serverDetails, diff --git a/commands/curation/curationaudit_test.go b/commands/curation/curationaudit_test.go index b50d1a4fb..c48caf096 100644 --- a/commands/curation/curationaudit_test.go +++ b/commands/curation/curationaudit_test.go @@ -682,7 +682,7 @@ func getTestCasesForDoCurationAudit() []testCase { restoreWD := testUtils.ChangeWDWithCallback(t, "tests/testdata/projects/package-managers") defer restoreWD() - curationCache, err := utils.GetCurationCacheFolderByTech(techutils.Gradle) + curationCache, err := utils.GetCurationCacheFolderByTech(techutils.Gradle.String()) require.NoError(t, err) return []string{ @@ -865,7 +865,7 @@ func getTestCasesForDoCurationAudit() []testCase { // The cache directory is determined by the project directory, so we need to "simulate" the cache directory when running the pretest build. // During the test, the blocked package will be resolved from the same cache directory that was populated in the pretest build. cleanUpTestDirChange := testUtils.ChangeWDWithCallback(t, filepath.Join("..", "test")) - curationCache, err := utils.GetCurationCacheFolderByTech(techutils.Maven) + curationCache, err := utils.GetCurationCacheFolderByTech(techutils.Maven.String()) require.NoError(t, err) cleanUpTestDirChange() return []string{"com.jfrog:maven-dep-tree:" + java.GetMavenDepTreeVersion() + ":tree", "-DdepsTreeOutputFile=output", "-Dmaven.repo.local=" + curationCache} diff --git a/commands/enrich/enrich.go b/commands/enrich/enrich.go index 74b068d8c..5a7628a04 100644 --- a/commands/enrich/enrich.go +++ b/commands/enrich/enrich.go @@ -185,7 +185,7 @@ func (enrichCmd *EnrichCommand) Run() (err error) { fileCollectingErr := fileCollectingErrorsQueue.GetError() if fileCollectingErr != nil { - scanResults.GeneralError = errors.Join(scanResults.GeneralError, fileCollectingErr) + scanResults.AddGeneralError(fileCollectingErr, false) } if scanResults.GetErrors() != nil { @@ -265,7 +265,7 @@ func (enrichCmd *EnrichCommand) createIndexerHandlerFunc(indexedFileProducer par return targetResults.AddTargetError(fmt.Errorf("%s failed to import graph: %s", logPrefix, err.Error()), false) } targetResults.ScaScanResults(scan.GetScaScansStatusCode(err, *scanResults), *scanResults) - targetResults.Technology = techutils.Technology(scanResults.ScannedPackageType) + targetResults.Technologies = []techutils.Technology{techutils.Technology(scanResults.ScannedPackageType)} return } diff --git a/commands/git/audit/gitaudit.go b/commands/git/audit/gitaudit.go index c7dc333ed..c7835af3a 100644 --- a/commands/git/audit/gitaudit.go +++ b/commands/git/audit/gitaudit.go @@ -11,7 +11,6 @@ import ( "github.com/jfrog/jfrog-client-go/xsc/services" sourceAudit "github.com/jfrog/jfrog-cli-security/commands/audit" - "github.com/jfrog/jfrog-cli-security/sca/bom/xrayplugin" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/results/output" @@ -53,7 +52,13 @@ func (gaCmd *GitAuditCommand) Run() (err error) { // No Error but no git info = project working tree is dirty return fmt.Errorf("detected uncommitted changes in '%s'. Please commit your changes and try again", gaCmd.repositoryLocalPath) } - gaCmd.gitContext = *gitInfo + gaCmd.SetGitContext(gitInfo) + // Get the config profile if applicable + configProfile, err := getJPDConfigProfile(gaCmd.GitAuditParams) + if err != nil { + return fmt.Errorf("failed to get config profile: %v", err) + } + gaCmd.SetConfigProfile(configProfile) // Run the scan auditResults := RunGitAudit(gaCmd.GitAuditParams) // Process the results and output @@ -66,6 +71,40 @@ func (gaCmd *GitAuditCommand) Run() (err error) { return sourceAudit.OutputResultsAndCmdError(auditResults, gaCmd.getResultWriter(auditResults), gaCmd.failBuild) } +func getJPDConfigProfile(params GitAuditParams) (*services.ConfigProfile, error) { + if !params.useConfigProfile { + // Not using config profile, return nil + log.Debug("Not using config profile for git audit as requested by the user") + return nil, nil + } + if params.configProfile != nil { + // Already set, use it + return params.configProfile, nil + } + log.Debug(fmt.Sprintf("Fetching config profile for git repo URL: %s", params.gitContext.Source.GitRepoHttpsCloneUrl)) + configProfile, err := xsc.GetConfigProfileByUrl(params.xrayVersion, params.serverDetails, params.gitContext.Source.GitRepoHttpsCloneUrl, params.resultsContext.ProjectKey) + if err != nil || configProfile == nil { + return nil, fmt.Errorf("failed to get config profile for git audit: %v", err) + } + return configProfile, verifyConfigProfile(configProfile) +} + +func verifyConfigProfile(configProfile *services.ConfigProfile) error { + if len(configProfile.Modules) != 1 { + return fmt.Errorf("more than one module was found '%s' profile. Frogbot currently supports only one module per config profile", configProfile.ProfileName) + } + if configProfile.Modules[0].PathFromRoot != "." { + return 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, configProfile.ProfileName, configProfile.Modules[0].PathFromRoot) + } + if profileString, err := utils.GetAsJsonString(configProfile, false, true); err != nil { + log.Verbose(fmt.Sprintf("Failed to get Config Profile as JSON string: %v", err)) + return nil + } else { + log.Verbose(fmt.Sprintf("Utilized Config Profile:\n%s", profileString)) + } + return nil +} + func DetectGitInfo(wd string) (gitInfo *services.XscGitInfoContext, err error) { scmManager, err := scm.DetectScmInProject(wd) if err != nil { @@ -74,7 +113,7 @@ func DetectGitInfo(wd string) (gitInfo *services.XscGitInfoContext, err error) { return scmManager.GetSourceControlContext() } -func toAuditParams(params GitAuditParams) *sourceAudit.AuditParams { +func toAuditParams(params GitAuditParams) (*sourceAudit.AuditParams, error) { auditParams := sourceAudit.NewAuditParams() // Connection params auditParams.SetServerDetails(params.serverDetails).SetInsecureTls(params.serverDetails.InsecureTls).SetXrayVersion(params.xrayVersion).SetXscVersion(params.xscVersion) @@ -97,6 +136,9 @@ func toAuditParams(params GitAuditParams) *sourceAudit.AuditParams { auditParams.SetGitContext(¶ms.gitContext).SetMultiScanId(params.multiScanId).SetStartTime(params.startTime) // Scan params auditParams.SetThreads(params.threads).SetWorkingDirs([]string{params.repositoryLocalPath}).SetExclusions(params.exclusions).SetScansToPerform(params.scansToPerform) + if params.useConfigProfile { + auditParams.SetConfigProfile(params.configProfile) + } // Output params auditParams.SetScansResultsOutputDir(params.outputDir).SetOutputFormat(params.outputFormat) auditParams.SetUploadCdxResults(params.uploadResults).SetRtResultRepository(params.rtResultRepository) @@ -104,13 +146,12 @@ func toAuditParams(params GitAuditParams) *sourceAudit.AuditParams { auditParams.SetBomGenerator(params.bomGenerator).SetScaScanStrategy(params.scaScanStrategy).SetViolationGenerator(params.violationGenerator) auditParams.SetCustomBomGenBinaryPath(params.customBomGenBinaryPath).SetCustomAnalyzerManagerBinaryPath(params.customAnalyzerManagerBinaryPath) // Basic params - isRecursiveScan := true - if _, ok := params.bomGenerator.(*xrayplugin.XrayLibBomGenerator); ok { - // 'Xray lib' BOM generator supports only one working directory, no recursive scan (single target) - isRecursiveScan = false + _, includeDirs, isRecursiveScan, err := sourceAudit.GetTargetsInfo(params.workingDirs, params.bomGenerator, params.scansToPerform, params.includeSbom, params.repositoryLocalPath) + if err != nil { + return nil, fmt.Errorf("failed to get targets info: %v", err) } - auditParams.SetUseJas(true).SetIsRecursiveScan(isRecursiveScan) - return auditParams + auditParams.SetWorkingDirs(includeDirs).SetUseJas(true).SetIsRecursiveScan(isRecursiveScan) + return auditParams, nil } func RunGitAudit(params GitAuditParams) (scanResults *results.SecurityCommandResults) { @@ -128,7 +169,11 @@ func RunGitAudit(params GitAuditParams) (scanResults *results.SecurityCommandRes params.multiScanId = multiScanId params.startTime = startTime // Run the scan - scanResults = sourceAudit.RunAudit(toAuditParams(params)) + auditParams, err := toAuditParams(params) + if err != nil { + return results.NewCommandResults(utils.SourceCode).AddGeneralError(err, false) + } + scanResults = sourceAudit.RunAudit(auditParams) // Send scan ended event xsc.SendScanEndedWithResults(params.serverDetails, scanResults) return scanResults diff --git a/commands/git/audit/gitauditparams.go b/commands/git/audit/gitauditparams.go index 9a0180bc7..54a7bdc2f 100644 --- a/commands/git/audit/gitauditparams.go +++ b/commands/git/audit/gitauditparams.go @@ -17,7 +17,9 @@ type GitAuditParams struct { // Git Params gitContext services.XscGitInfoContext // Connection params - serverDetails *config.ServerDetails + serverDetails *config.ServerDetails + useConfigProfile bool + configProfile *services.ConfigProfile // Violations params resultsContext results.ResultContext failBuild bool @@ -25,6 +27,7 @@ type GitAuditParams struct { scansToPerform []utils.SubScanType includeSbom bool threads int + workingDirs []string exclusions []string // Output params outputFormat format.OutputFormat @@ -134,6 +137,11 @@ func (gap *GitAuditParams) SetExclusions(exclusions []string) *GitAuditParams { return gap } +func (gap *GitAuditParams) SetWorkingDirs(workingDirs []string) *GitAuditParams { + gap.workingDirs = workingDirs + return gap +} + func (gap *GitAuditParams) SetSbomGenerator(generator bom.SbomGenerator) *GitAuditParams { gap.bomGenerator = generator return gap @@ -198,3 +206,29 @@ func (gap *GitAuditParams) SetCustomBomGenBinaryPath(customBomGenBinaryPath stri func (gap *GitAuditParams) CustomBomGenBinaryPath() string { return gap.customBomGenBinaryPath } + +func (gap *GitAuditParams) SetUseConfigProfile(useConfigProfile bool) *GitAuditParams { + gap.useConfigProfile = useConfigProfile + return gap +} + +func (gap *GitAuditParams) UseConfigProfile() bool { + return gap.useConfigProfile +} + +func (gap *GitAuditParams) SetConfigProfile(configProfile *services.ConfigProfile) *GitAuditParams { + gap.configProfile = configProfile + return gap +} + +func (gap *GitAuditParams) ConfigProfile() *services.ConfigProfile { + return gap.configProfile +} + +func (gap *GitAuditParams) SetGitContext(gitContext *services.XscGitInfoContext) *GitAuditParams { + if gitContext == nil { + return gap + } + gap.gitContext = *gitContext + return gap +} diff --git a/commands/maliciousscan/maliciousscan.go b/commands/maliciousscan/maliciousscan.go index 3386bd4de..06e43cc68 100644 --- a/commands/maliciousscan/maliciousscan.go +++ b/commands/maliciousscan/maliciousscan.go @@ -153,7 +153,6 @@ func (cmd *MaliciousScanCommand) initializeCommandResults(xrayVersion string, en func (cmd *MaliciousScanCommand) createJasScanner() (*jas.JasScanner, error) { scannerOptions := []jas.JasScannerOption{ jas.WithEnvVars( - false, jas.NotDiffScanEnvValue, jas.GetAnalyzerManagerXscEnvVars(false, "", "", "", cmd.project, nil), ), @@ -178,7 +177,7 @@ func (cmd *MaliciousScanCommand) createJasScanner() (*jas.JasScanner, error) { func (cmd *MaliciousScanCommand) setAnalyzerManagerPath(scanner *jas.JasScanner) error { if cmd.customAnalyzerManagerPath == "" { - if err := jas.DownloadAnalyzerManagerIfNeeded(0); err != nil { + if err := jas.DownloadAnalyzerManagerIfNeeded("", nil, 0); err != nil { return fmt.Errorf("failed to download analyzer manager: %s", err.Error()) } var err error diff --git a/commands/sast_server/sast_server_test.go b/commands/sast_server/sast_server_test.go index df02d399c..ff95d796b 100644 --- a/commands/sast_server/sast_server_test.go +++ b/commands/sast_server/sast_server_test.go @@ -34,7 +34,7 @@ func getFreePort() (int, error) { } func TestRunSastServerHappyFlow(t *testing.T) { - assert.NoError(t, jas.DownloadAnalyzerManagerIfNeeded(0)) + assert.NoError(t, jas.DownloadAnalyzerManagerIfNeeded("", nil, 0)) mockServer, serverDetails, _ := validations.XrayServer(t, validations.MockServerParams{XrayVersion: utils.EntitlementsMinVersion}) defer mockServer.Close() scanner, initError := jas.NewJasScanner(serverDetails) diff --git a/commands/scan/scan.go b/commands/scan/scan.go index 0dfe2e4c8..099f09465 100644 --- a/commands/scan/scan.go +++ b/commands/scan/scan.go @@ -338,7 +338,7 @@ func (scanCmd *ScanCommand) initScanCmdResults(cmdType utils.CommandType) (xrayM } else { cmdResults.SetEntitledForJas(entitledForJas) if entitledForJas { - if utils.IsJASRequested(cmdResults.CmdType, scanCmd.scansToPerform...) { + if cmdResults.IsJASRequested(scanCmd.scansToPerform...) { if err = jas.ValidateRequiredInstalledSoftware(); err != nil { return xrayManager, cmdResults.AddGeneralError(err, false) } @@ -352,10 +352,10 @@ func (scanCmd *ScanCommand) initScanCmdResults(cmdType utils.CommandType) (xrayM func (scanCmd *ScanCommand) prepareForScan(cmdResults *results.SecurityCommandResults, xrayManager *xrayClient.XrayServicesManager) (err error) { // Download (if needed) the analyzer manager in a background routine. AnalyzerErrGroup := new(errgroup.Group) - if cmdResults.Entitlements.Jas && utils.IsJASRequested(cmdResults.CmdType, scanCmd.scansToPerform...) { + if cmdResults.Entitlements.Jas && cmdResults.IsJASRequested(scanCmd.scansToPerform...) { if scanCmd.customAnalyzerManagerPath == "" { AnalyzerErrGroup.Go(func() error { - return jas.DownloadAnalyzerManagerIfNeeded(0) + return jas.DownloadAnalyzerManagerIfNeeded("", nil, 0) }) } else { log.Debug("Using custom analyzer manager binary path, skipping download") @@ -365,7 +365,7 @@ func (scanCmd *ScanCommand) prepareForScan(cmdResults *results.SecurityCommandRe } // Prepare the BOM generator for SCA scans in a background routine. scaErrGroup := new(errgroup.Group) - if cmdResults.ResultContext.IncludeSbom || utils.IsScanRequested(cmdResults.CmdType, utils.ScaScan, scanCmd.scansToPerform...) { + if cmdResults.ResultContext.IncludeSbom || utils.IsScanRequested(cmdResults.CmdType, utils.ScaScan, cmdResults.IsScanRequestedByCentralConfig(utils.ScaScan), scanCmd.scansToPerform...) { scaErrGroup.Go(func() error { return scanCmd.bomGenerator.WithOptions(indexer.WithXray(xrayManager, scanCmd.xrayVersion), indexer.WithBypassArchiveLimits(scanCmd.bypassArchiveLimits)).PrepareGenerator() }) @@ -432,8 +432,10 @@ func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, cmdResults targetResults := scanCmd.getBinaryTargetResults(cmdResults, filePath, threadId) // Generate SBOM for the file. deprecatedGraph := scanCmd.GenerateBinarySbom(cmdResults.CmdType, targetResults, cmdResults.ResultContext.IncludeSbom, threadId) - if len(targetResults.Errors) > 0 { - log.Warn(fmt.Sprintf(clientutils.GetLogMsgPrefix(threadId, false)+"Failed to generate SBOM for file %s: %s", targetResults.Target, targetResults.GetErrors())) + if notSkippedErrors := targetResults.GetNotSkippedErrors(); len(notSkippedErrors) > 0 { + for _, notSkippedError := range notSkippedErrors { + log.Warn(fmt.Sprintf(clientutils.GetLogMsgPrefix(threadId, false)+"Failed to generate SBOM for file %s: %s", targetResults.Target, notSkippedError.Error())) + } return } // Add a new task to the second producer/consumer @@ -457,7 +459,7 @@ func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, cmdResults } func (scanCmd *ScanCommand) GenerateBinarySbom(cmdType utils.CommandType, targetResults *results.TargetResults, includeSbom bool, threadId int) (deprecatedGraph *xrayClientUtils.BinaryGraphNode) { - if !includeSbom && !utils.IsScanRequested(cmdType, utils.ScaScan, scanCmd.scansToPerform...) { + if !includeSbom && !utils.IsScanRequested(cmdType, utils.ScaScan, targetResults.IsScanRequestedByCentralConfig(utils.ScaScan), scanCmd.scansToPerform...) { log.Debug(clientutils.GetLogMsgPrefix(threadId, false) + "Skipping SBOM generation as requested by input...") return } @@ -486,8 +488,8 @@ func (scanCmd *ScanCommand) GenerateBinarySbom(cmdType utils.CommandType, target func (scanCmd *ScanCommand) RunBinaryScaScan(fileTarget string, cmdResults *results.SecurityCommandResults, targetResults *results.TargetResults, deprecatedGraph *xrayClientUtils.BinaryGraphNode, scanThreadId int) (targetCompId string, graphScanResults *services.ScanResponse, err error) { scanLogPrefix := clientutils.GetLogMsgPrefix(scanThreadId, false) // If the scan is not requested, skip it. - if !utils.IsScanRequested(cmdResults.CmdType, utils.ScaScan, scanCmd.scansToPerform...) { - log.Debug(scanLogPrefix + fmt.Sprintf("Skipping SCA for %s as requested by input...", targetResults.Target)) + if !utils.IsScanRequested(cmdResults.CmdType, utils.ScaScan, targetResults.IsScanRequestedByCentralConfig(utils.ScaScan), scanCmd.scansToPerform...) { + log.Debug(scanLogPrefix + fmt.Sprintf("Skipping SCA for '%s' as requested by input...", targetResults.String())) return } // TODO: Use the following code when binary will also support scan strategy (interface) and not only scan graph. @@ -539,7 +541,7 @@ func (scanCmd *ScanCommand) RunBinaryScaScan(fileTarget string, cmdResults *resu return } targetResults.ScaScanResults(scan.GetScaScansStatusCode(err, *graphScanResults), *graphScanResults) - targetResults.Technology = techutils.ToTechnology(graphScanResults.ScannedPackageType) + targetResults.Technologies = []techutils.Technology{techutils.ToTechnology(graphScanResults.ScannedPackageType)} // Dump scan response if requested if scanCmd.outputDir == "" { return @@ -570,14 +572,14 @@ func (scanCmd *ScanCommand) getXrayScanGraphParams(msi string) *scangraph.ScanGr func (scanCmd *ScanCommand) RunBinaryJasScans(cmdType utils.CommandType, msi string, secretValidation bool, targetResults *results.TargetResults, targetCompId string, graphScanResults *services.ScanResponse, jasFileProducerConsumer *utils.SecurityParallelRunner, scanThreadId int) (err error) { scanLogPrefix := clientutils.GetLogMsgPrefix(scanThreadId, false) - module, err := getJasModule(targetResults) + deprecatedAppsConfigModule, err := getJasDeprecatedAppsConfigModule(targetResults.Target) if err != nil { return targetResults.AddTargetError(fmt.Errorf(scanLogPrefix+"jas scanning failed with error: %s", err.Error()), false) } + targetResults.DeprecatedAppsConfigModule = &deprecatedAppsConfigModule // Prepare Jas scans scannerOptions := []jas.JasScannerOption{ jas.WithEnvVars( - secretValidation, jas.NotDiffScanEnvValue, jas.GetAnalyzerManagerXscEnvVars( false, @@ -607,12 +609,12 @@ func (scanCmd *ScanCommand) RunBinaryJasScans(cmdType utils.CommandType, msi str } log.Debug(fmt.Sprintf("Using analyzer manager executable at: %s", scanner.AnalyzerManager.AnalyzerManagerFullPath)) jasParams := runner.JasRunnerParams{ - Runner: jasFileProducerConsumer, - ServerDetails: scanCmd.serverDetails, - Scanner: scanner, - Module: module, - TargetOutputDir: scanCmd.outputDir, - ScansToPerform: scanCmd.scansToPerform, + Runner: jasFileProducerConsumer, + ServerDetails: scanCmd.serverDetails, + Scanner: scanner, + SecretValidation: secretValidation, + TargetOutputDir: scanCmd.outputDir, + ScansToPerform: scanCmd.scansToPerform, CvesProvider: func() (directCves []string, indirectCves []string) { if graphScanResults == nil { // No SCA scan results, return empty CVE lists. @@ -649,11 +651,11 @@ func getJasScanTypes(cmdType utils.CommandType, targetResults *results.TargetRes } func isDockerBinary(cmdType utils.CommandType, targetResults *results.TargetResults) bool { - return cmdType == utils.DockerImage || targetResults.Technology == techutils.Docker || targetResults.Technology == techutils.Oci + return cmdType == utils.DockerImage || targetResults.HasTechnology(techutils.Docker) || targetResults.HasTechnology(techutils.Oci) } -func getJasModule(targetResults *results.TargetResults) (jfrogappsconfig.Module, error) { - jfrogAppsConfig, err := jas.CreateJFrogAppsConfig([]string{targetResults.Target}) +func getJasDeprecatedAppsConfigModule(target string) (jfrogappsconfig.Module, error) { + jfrogAppsConfig, err := jas.CreateJFrogAppsConfig([]string{target}) if err != nil { return jfrogappsconfig.Module{}, err } diff --git a/commands/source_mcp/source_mcp_test.go b/commands/source_mcp/source_mcp_test.go index ab55ae49b..1771f1eec 100644 --- a/commands/source_mcp/source_mcp_test.go +++ b/commands/source_mcp/source_mcp_test.go @@ -12,7 +12,7 @@ import ( ) func TestRunSourceMcpHappyFlow(t *testing.T) { - assert.NoError(t, jas.DownloadAnalyzerManagerIfNeeded(0)) + assert.NoError(t, jas.DownloadAnalyzerManagerIfNeeded("", nil, 0)) mockServer, serverDetails, _ := validations.XrayServer(t, validations.MockServerParams{XrayVersion: utils.EntitlementsMinVersion}) defer mockServer.Close() scanner, initError := jas.NewJasScanner(serverDetails) @@ -44,7 +44,7 @@ func TestRunSourceMcpHappyFlow(t *testing.T) { } func TestRunSourceMcpScannerError(t *testing.T) { - assert.NoError(t, jas.DownloadAnalyzerManagerIfNeeded(0)) + assert.NoError(t, jas.DownloadAnalyzerManagerIfNeeded("", nil, 0)) mockServer, serverDetails, _ := validations.XrayServer(t, validations.MockServerParams{XrayVersion: utils.EntitlementsMinVersion}) defer mockServer.Close() scanner, initError := jas.NewJasScanner(serverDetails) diff --git a/git_test.go b/git_test.go index 616dd19ba..8f6717078 100644 --- a/git_test.go +++ b/git_test.go @@ -58,13 +58,16 @@ func TestCountContributorsFlags(t *testing.T) { type gitAuditCommandTestParams struct { auditCommandTestParams + UseConfigProfile bool // Override the test project repo clone url OverrideRepoCloneUrl string OverrideCommitMsg string } -func testGitAuditCommand(t *testing.T, params auditCommandTestParams) (string, error) { - return securityTests.PlatformCli.RunCliCmdWithOutputs(t, append([]string{"git"}, getAuditCmdArgs(params)...)...) +func testGitAuditCommand(t *testing.T, params gitAuditCommandTestParams) (string, error) { + args := append([]string{"git"}, getAuditCmdArgs(params.auditCommandTestParams)...) + args = append(args, fmt.Sprintf("--use-config-profile=%t", params.UseConfigProfile)) + return securityTests.PlatformCli.RunCliCmdWithOutputs(t, args...) } func getDummyGitRepoUrl() string { @@ -104,7 +107,7 @@ func createTestProjectRunGitAuditAndValidate(t *testing.T, projectPath string, g amendHeadCommitForTest(t, gitAuditParams.OverrideCommitMsg) } // Run the audit command with git repo and verify violations are reported to the platform. - output, err := testGitAuditCommand(t, gitAuditParams.auditCommandTestParams) + output, err := testGitAuditCommand(t, gitAuditParams) if expectError != "" { assert.ErrorContains(t, err, expectError) } else { @@ -291,9 +294,15 @@ func TestGitAuditJasSkipNotApplicableCvesViolations(t *testing.T) { // Run the git audit command and verify violations are reported to the platform. createTestProjectRunGitAuditAndValidate(t, projectPath, gitAuditCommandTestParams{ - auditCommandTestParams: auditCommandTestParams{Format: format.SimpleJson, Watches: []string{watchName}, DisableFailOnFailedBuildFlag: true}, - OverrideRepoCloneUrl: dummyCloneUrl, - OverrideCommitMsg: getDummyCommitMsg("git-audit-jas-skip-not-applicable-cves-violations-before"), + auditCommandTestParams: auditCommandTestParams{ + Format: format.SimpleJson, + Watches: []string{watchName}, + DisableFailOnFailedBuildFlag: true, + OnlyScan: []securityUtils.SubScanType{securityUtils.SecretsScan, securityUtils.ScaScan, securityUtils.SastScan, securityUtils.IacScan}, + ValidateSecrets: true, + }, + OverrideRepoCloneUrl: dummyCloneUrl, + OverrideCommitMsg: getDummyCommitMsg("git-audit-jas-skip-not-applicable-cves-violations-before"), }, xrayVersion, xscVersion, "", validations.ValidationParams{ @@ -320,9 +329,15 @@ func TestGitAuditJasSkipNotApplicableCvesViolations(t *testing.T) { // Run the audit command with git repo and verify violations are reported to the platform and not applicable issues are skipped. createTestProjectRunGitAuditAndValidate(t, projectPath, gitAuditCommandTestParams{ - auditCommandTestParams: auditCommandTestParams{Format: format.SimpleJson, Watches: []string{skipWatchName}, DisableFailOnFailedBuildFlag: true}, - OverrideRepoCloneUrl: dummyCloneUrl, - OverrideCommitMsg: getDummyCommitMsg("git-audit-jas-skip-not-applicable-cves-violations-after"), + auditCommandTestParams: auditCommandTestParams{ + Format: format.SimpleJson, + Watches: []string{skipWatchName}, + DisableFailOnFailedBuildFlag: true, + ValidateSecrets: true, + OnlyScan: []securityUtils.SubScanType{securityUtils.SecretsScan, securityUtils.ScaScan, securityUtils.SastScan, securityUtils.IacScan}, + }, + OverrideRepoCloneUrl: dummyCloneUrl, + OverrideCommitMsg: getDummyCommitMsg("git-audit-jas-skip-not-applicable-cves-violations-after"), }, xrayVersion, xscVersion, "", validations.ValidationParams{ diff --git a/go.mod b/go.mod index 15db7b962..10d3b0c80 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/jfrog/jfrog-cli-security go 1.26.3 require ( - github.com/CycloneDX/cyclonedx-go v0.10.0 + github.com/CycloneDX/cyclonedx-go v0.11.0 github.com/beevik/etree v1.6.0 github.com/go-git/go-git/v5 v5.19.1 github.com/google/go-github/v56 v56.0.0 @@ -11,22 +11,22 @@ require ( github.com/gookit/color v1.6.1 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.6.3 - github.com/jfrog/build-info-go v1.13.1-0.20260521104402-1e35b9b5b0c6 + github.com/jfrog/build-info-go v1.13.1-0.20260528065004-80409c046540 github.com/jfrog/froggit-go v1.22.0 github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-apps-config v1.0.1 - github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260526100850-f85282fe6d9b - github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260522091649-43f236276873 - github.com/jfrog/jfrog-client-go v1.55.1-0.20260522071027-8b60a715d6e4 + github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260528073225-e2d59f90c8c6 + github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260528061115-b41c87af0194 + github.com/jfrog/jfrog-client-go v1.55.1-0.20260528115006-6ca9682a3255 github.com/magiconair/properties v1.8.10 github.com/owenrumney/go-sarif/v3 v3.2.3 github.com/package-url/packageurl-go v0.1.3 github.com/stretchr/testify v1.11.1 github.com/urfave/cli v1.22.17 github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 - golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f + golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.44.0 + golang.org/x/sys v0.45.0 golang.org/x/text v0.37.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -38,7 +38,7 @@ require ( github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/andybalholm/brotli v1.2.0 // indirect + github.com/andybalholm/brotli v1.2.1 // indirect github.com/buger/jsonparser v1.2.0 // indirect github.com/c-bata/go-prompt v0.2.6 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -62,14 +62,14 @@ require ( github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/forPelevin/gomoji v1.4.1 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/gfleury/go-bitbucket-v1 v0.0.0-20240917142304-df385efaac68 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect - github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 // indirect + github.com/gocarina/gocsv v0.0.0-20260523204920-c264028e67ea // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -85,15 +85,15 @@ require ( github.com/jedib0t/go-pretty/v6 v6.7.10 // indirect github.com/jfrog/archiver/v3 v3.6.3 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect - github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/ktrysmt/go-bitbucket v0.9.88 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.21 // indirect - github.com/mattn/go-tty v0.0.7 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mattn/go-tty v0.0.8 // indirect github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect @@ -101,11 +101,11 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/moby/api v1.54.0 // indirect github.com/moby/moby/client v0.3.0 // indirect - github.com/nwaples/rardecode/v2 v2.2.2 // indirect + github.com/nwaples/rardecode/v2 v2.2.3 // indirect github.com/oklog/run v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pelletier/go-toml/v2 v2.3.0 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect @@ -124,7 +124,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/ulikunitz/xz v0.5.15 // indirect github.com/vbatts/tar-split v0.12.2 // indirect - github.com/vbauerster/mpb/v8 v8.12.0 // indirect + github.com/vbauerster/mpb/v8 v8.12.1 // indirect github.com/xanzy/go-gitlab v0.115.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect @@ -138,9 +138,9 @@ require ( go.opentelemetry.io/otel/metric v1.42.0 // indirect go.opentelemetry.io/otel/trace v1.42.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.50.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/term v0.43.0 // indirect golang.org/x/time v0.15.0 // indirect @@ -153,6 +153,8 @@ require ( // replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go master +replace github.com/CycloneDX/cyclonedx-go => github.com/CycloneDX/cyclonedx-go v0.10.0 + // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 master //replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory main diff --git a/go.sum b/go.sum index a770b12ed..967a857f0 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -88,8 +88,8 @@ github.com/forPelevin/gomoji v1.4.1 h1:7U+Bl8o6RV/dOQz7coQFWj/jX6Ram6/cWFOuFDEPE github.com/forPelevin/gomoji v1.4.1/go.mod h1:mM6GtmCgpoQP2usDArc6GjbXrti5+FffolyQfGgPboQ= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/gfleury/go-bitbucket-v1 v0.0.0-20240917142304-df385efaac68 h1:iJXWkoIPk3e8RVHhQE/gXfP2TP3OLQ9vVPNSJ+oL6mM= github.com/gfleury/go-bitbucket-v1 v0.0.0-20240917142304-df385efaac68/go.mod h1:bB7XwdZF40tLVnu9n5A9TjI2ddNZtLYImtwYwmcmnRo= github.com/gfleury/go-bitbucket-v1/test/bb-mock-server v0.0.0-20230825095122-9bc1711434ab h1:BeG9dDWckFi/p5Gvqq3wTEDXsUV4G6bdvjEHMOT2B8E= @@ -111,8 +111,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= -github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= +github.com/gocarina/gocsv v0.0.0-20260523204920-c264028e67ea h1:XvL0wVLiLmxbUB0xbPE3vY70Qrk0bkCdD8h7SL1Hyl4= +github.com/gocarina/gocsv v0.0.0-20260523204920-c264028e67ea/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= @@ -161,27 +161,27 @@ github.com/jedib0t/go-pretty/v6 v6.7.10 h1:B/2qW2Bkv2L6n14PP8o1kx75kWzHOQ3YTluWz github.com/jedib0t/go-pretty/v6 v6.7.10/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jfrog/archiver/v3 v3.6.3 h1:hkAmPjBw393tPmQ07JknLNWFNZjXdy2xFEnOW9wwOxI= github.com/jfrog/archiver/v3 v3.6.3/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg= -github.com/jfrog/build-info-go v1.13.1-0.20260521104402-1e35b9b5b0c6 h1:x9UKlP7rEzCYS/Z8IP0I+Oy8PTPpOxFy8qpMeRMbrwo= -github.com/jfrog/build-info-go v1.13.1-0.20260521104402-1e35b9b5b0c6/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE= +github.com/jfrog/build-info-go v1.13.1-0.20260528065004-80409c046540 h1:yJjTgSfmsBx9Q6/iiJxXQ/m0KZfFjNx8nNzaRLCM7z4= +github.com/jfrog/build-info-go v1.13.1-0.20260528065004-80409c046540/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE= github.com/jfrog/froggit-go v1.22.0 h1:eeN5F8sOUo+h2cXkzArAu4nvSdjkDTAZtgqwrct70qg= github.com/jfrog/froggit-go v1.22.0/go.mod h1:wRDryqyp3oe+eHgME2mpnEQmO8XBECIPagFwj0nHmdI= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= 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-artifactory v0.8.1-0.20260526100850-f85282fe6d9b h1:qn1wHa1SXeost9hRj3A0tvRQQa5+JGiWmfVzMMvmiEc= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260526100850-f85282fe6d9b/go.mod h1:QTAlyhazt1yITTf72eiEfwAdM2xsbE26LmOqaN4wFJc= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260522091649-43f236276873 h1:6X1Hwu0st7c9gbFoIj1fc8qjoQ3wAHWX2qo7K9IxWgU= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260522091649-43f236276873/go.mod h1:D9afcOJmauUYcQZ3WGDg7HejyoBmCQr2XrwXHeN1YY8= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260522071027-8b60a715d6e4 h1:ujVu255rk51l9Uz1t75DdsVoa2MH+lYNV2cB2xDWjPM= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260522071027-8b60a715d6e4/go.mod h1:k3PqoFpS6XDt9/4xg3pS8J8JUvxtaz1w2vdTdodknGk= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260528073225-e2d59f90c8c6 h1:E2oWXSoOPzBvrh+SL4IrlmnddasBQinjPSbFfKwhIYg= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260528073225-e2d59f90c8c6/go.mod h1:GQEGVW3wT1XPykXNsEiPQrF8/+01JvDVcGGYb5vqJuE= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260528061115-b41c87af0194 h1:cwppCKLitT0XBqYGQimW00qyx1ej88sY+rIjXAWNvAU= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260528061115-b41c87af0194/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= +github.com/jfrog/jfrog-client-go v1.55.1-0.20260528115006-6ca9682a3255 h1:CIOMO1Hj5N6PaIu7sJZ9bPowcibkcaWDulM2R6LHO9o= +github.com/jfrog/jfrog-client-go v1.55.1-0.20260528115006-6ca9682a3255/go.mod h1:FHpjN1nTDoj96xd6obe27EOgGErqzU0rQgC96L3Ch9E= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= @@ -210,15 +210,15 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= -github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= -github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= -github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= +github.com/mattn/go-tty v0.0.8 h1:yxtc0Ye17/1ne/bjy993YUoyP8bJJFa9n5M9XTdwoZQ= +github.com/mattn/go-tty v0.0.8/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 h1:mmJCWLe63QvybxhW1iBmQWEaCKdc4SKgALfTNZ+OphU= github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0/go.mod h1:mDunUZ1IUJdJIRHvFb+LPBUtxe3AYB5MI6BMXNg8194= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= @@ -234,8 +234,8 @@ github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= -github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU= -github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nwaples/rardecode/v2 v2.2.3 h1:qaVuy3ChZDbAQZshPLjHeNJKF3Cru8uo9jmgveKIy2A= +github.com/nwaples/rardecode/v2 v2.2.3/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= @@ -248,8 +248,8 @@ github.com/owenrumney/go-sarif/v3 v3.2.3 h1:n6mdX5ugKwCrZInvBsf6WumXmpAe3mbmQXgk github.com/owenrumney/go-sarif/v3 v3.2.3/go.mod h1:1bV7t8SZg7pX41spaDkEUs8/yEjzk9JapztMoX1XNjg= github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= -github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= -github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= +github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= @@ -311,8 +311,8 @@ github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= -github.com/vbauerster/mpb/v8 v8.12.0 h1:+gneY3ifzc88tKDzOtfG8k8gfngCx615S2ZmFM4liWg= -github.com/vbauerster/mpb/v8 v8.12.0/go.mod h1:V02YIuMVo301Y1VE9VtZlD8s84OMsk+EKN6mwvf/588= +github.com/vbauerster/mpb/v8 v8.12.1 h1:pyj3yQ2ZGQJgUXm4h17QpR+eERaNz5OQ1ftPSEE/sMM= +github.com/vbauerster/mpb/v8 v8.12.1/go.mod h1:XLXRfStkw/6i5k0aQltijDHT1Z93fD1DVwmIdcFUp6k= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8= @@ -354,14 +354,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3 h1:VHEvKbpgPXcPXn40t9cDTGK3JZwMikIEyF/CTrFfu7k= +golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -372,8 +372,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= @@ -407,12 +407,11 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -436,8 +435,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= diff --git a/jas/analyzermanager.go b/jas/analyzermanager.go index 6a09d3bab..9492930f6 100644 --- a/jas/analyzermanager.go +++ b/jas/analyzermanager.go @@ -213,7 +213,7 @@ func GetAnalyzerManagerExitCode(err error) int { // Download the latest AnalyzerManager executable if not cached locally. // By default, the zip is downloaded directly from jfrog releases. -func DownloadAnalyzerManagerIfNeeded(threadId int) error { +func DownloadAnalyzerManagerIfNeeded(remoteRepo string, remoteServerDetails *config.ServerDetails, threadId int) error { downloadPath, err := GetAnalyzerManagerDownloadPath() if err != nil { return err @@ -222,7 +222,7 @@ func DownloadAnalyzerManagerIfNeeded(threadId int) error { if err != nil { return err } - return utils.DownloadResourceFromPlatformIfNeeded("Analyzer Manager", downloadPath, analyzerManagerDir, AnalyzerManagerZipName, true, threadId) + return utils.DownloadResourceFromPlatformIfNeeded("Analyzer Manager", downloadPath, analyzerManagerDir, AnalyzerManagerZipName, true, remoteRepo, remoteServerDetails, threadId) } func establishPipeToFile(dst io.WriteCloser, src io.Reader) { @@ -331,7 +331,7 @@ func RunAnalyzerManagerWithPipes(env map[string]string, cmd string, inputPipe io // RunAnalyzerManagerWithPipesAndDownload downloads the analyzer manager if needed and runs the command with pipes. func RunAnalyzerManagerWithPipesAndDownload(envVars map[string]string, cmd string, inputPipe io.Reader, outputPipe io.Writer, errorPipe io.Writer, timeout int, args ...string) error { - if err := DownloadAnalyzerManagerIfNeeded(0); err != nil { + if err := DownloadAnalyzerManagerIfNeeded("", nil, 0); err != nil { return fmt.Errorf("failed to download Analyzer Manager: %w", err) } return RunAnalyzerManagerWithPipes(envVars, cmd, inputPipe, outputPipe, errorPipe, timeout, args...) diff --git a/jas/applicability/applicabilitymanager.go b/jas/applicability/applicabilitymanager.go index 95c10b90b..2100ac477 100644 --- a/jas/applicability/applicabilitymanager.go +++ b/jas/applicability/applicabilitymanager.go @@ -10,6 +10,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" @@ -44,7 +45,7 @@ type ContextualAnalysisScanParams struct { ThirdPartyContextualAnalysis bool ThreadId int TargetCount int - Module jfrogappsconfig.Module + Target results.ScanTarget } // The getApplicabilityScanResults function runs the applicability scan flow, which includes the following steps: @@ -63,9 +64,8 @@ func RunApplicabilityScan(params ContextualAnalysisScanParams, scanner *jas.JasS return } startTime := time.Now() - log.Info(jas.GetStartJasScanLog(utils.ContextualAnalysisScan, params.ThreadId, params.Module, params.TargetCount)) - // Applicability scan does not produce violations. - if results, _, err = applicabilityScanManager.scanner.Run(applicabilityScanManager, params.Module); err != nil { + log.Info(jas.GetStartJasScanLog(utils.ContextualAnalysisScan, params.ThreadId, params.Target.DeprecatedAppsConfigModule, params.TargetCount)) + if results, err = applicabilityScanManager.runApplicabilityScan(params); err != nil { return } applicableCveCount := sarifutils.GetRulesPropertyCount("applicability", "applicable", results...) @@ -75,6 +75,17 @@ func RunApplicabilityScan(params ContextualAnalysisScanParams, scanner *jas.JasS return } +func (applicabilityScanManager *ApplicabilityScanManager) runApplicabilityScan(params ContextualAnalysisScanParams) (vulnerabilitiesSarifRuns []*sarif.Run, err error) { + if params.Target.DeprecatedAppsConfigModule == nil { + // Applicability scan does not produce violations. + vulnerabilitiesSarifRuns, _, err = applicabilityScanManager.scanner.Run(applicabilityScanManager, params.Target) + return + } + // Applicability scan does not produce violations. + vulnerabilitiesSarifRuns, _, err = applicabilityScanManager.scanner.DeprecatedRun(applicabilityScanManager, *params.Target.DeprecatedAppsConfigModule, params.Target.GetCentralConfigExclusions(utils.ContextualAnalysisScan)) + return +} + func newApplicabilityScanManager(directDependenciesCves, indirectDependenciesCves []string, scanner *jas.JasScanner, thirdPartyScan bool, scanType ApplicabilityScanType, scannerTempDir string) (manager *ApplicabilityScanManager) { return &ApplicabilityScanManager{ directDependenciesCves: directDependenciesCves, @@ -87,14 +98,24 @@ func newApplicabilityScanManager(directDependenciesCves, indirectDependenciesCve } } -func (asm *ApplicabilityScanManager) Run(module jfrogappsconfig.Module) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { - if err = asm.createConfigFile(module, asm.scanner.ScannersExclusions.ContextualAnalysisExcludePatterns, asm.scanner.Exclusions...); err != nil { +func (asm *ApplicabilityScanManager) DeprecatedRun(module jfrogappsconfig.Module, centralConfigExclusions []string) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = asm.deprecatedCreateConfigFile(module, centralConfigExclusions, asm.scanner.Exclusions...); err != nil { return } if err = asm.runAnalyzerManager(); err != nil { return } - return jas.ReadJasScanRunsFromFile(asm.resultsFileName, module.SourceRoot, applicabilityDocsUrlSuffix, asm.scanner.MinSeverity) + return jas.ReadJasScanRunsFromFile(asm.resultsFileName, applicabilityDocsUrlSuffix, asm.scanner.MinSeverity, module.SourceRoot) +} + +func (asm *ApplicabilityScanManager) Run(target results.ScanTarget) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = asm.createConfigFileForTarget(target); err != nil { + return + } + if err = asm.runAnalyzerManager(); err != nil { + return + } + return jas.ReadJasScanRunsFromFile(asm.resultsFileName, applicabilityDocsUrlSuffix, asm.scanner.MinSeverity, target.Target, target.Include...) } func (asm *ApplicabilityScanManager) cvesExists() bool { @@ -116,12 +137,34 @@ type scanConfiguration struct { ScanType string `yaml:"scantype"` } -func (asm *ApplicabilityScanManager) createConfigFile(module jfrogappsconfig.Module, centralConfigExclusions []string, exclusions ...string) error { +func (asm *ApplicabilityScanManager) createConfigFileForTarget(target results.ScanTarget) error { + excludePatterns := jas.GetJasExcludePatternsForTarget(target, target.GetCentralConfigExclusions(utils.ContextualAnalysisScan)) + if asm.thirdPartyScan { + log.Info("Including node modules folder in applicability scan") + excludePatterns = removeElementFromSlice(excludePatterns, utils.NodeModulesPattern) + } + configFileContent := applicabilityScanConfig{ + Scans: []scanConfiguration{ + { + Roots: jas.GetRootsFromTarget(target), + Output: asm.resultsFileName, + Type: asm.commandType, + GrepDisable: false, + CveWhitelist: asm.directDependenciesCves, + IndirectCveWhitelist: asm.indirectDependenciesCves, + SkippedDirs: excludePatterns, + }, + }, + } + return jas.CreateScannersConfigFile(asm.configFileName, configFileContent, jasutils.Applicability) +} + +func (asm *ApplicabilityScanManager) deprecatedCreateConfigFile(module jfrogappsconfig.Module, centralConfigExclusions []string, exclusions ...string) error { roots, err := jas.GetSourceRoots(module, nil) if err != nil { return err } - excludePatterns := jas.GetExcludePatterns(module, nil, centralConfigExclusions, exclusions...) + excludePatterns := jas.GetJasExcludePatterns(module, nil, centralConfigExclusions, exclusions...) if asm.thirdPartyScan { log.Info("Including node modules folder in applicability scan") excludePatterns = removeElementFromSlice(excludePatterns, utils.NodeModulesPattern) diff --git a/jas/applicability/applicabilitymanager_test.go b/jas/applicability/applicabilitymanager_test.go index 5d3286512..f62c59bd7 100644 --- a/jas/applicability/applicabilitymanager_test.go +++ b/jas/applicability/applicabilitymanager_test.go @@ -163,7 +163,7 @@ func TestNewApplicabilityScanManager_VulnerabilitiesDontExist(t *testing.T) { } } -func TestCreateConfigFile_VerifyFileWasCreated(t *testing.T) { +func TestApplicabilityScan_CreateDeprecatedConfigFile_VerifyFileWasCreated(t *testing.T) { // Arrange scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() @@ -175,7 +175,7 @@ func TestCreateConfigFile_VerifyFileWasCreated(t *testing.T) { currWd, err := coreutils.GetWorkingDirectory() assert.NoError(t, err) - err = applicabilityManager.createConfigFile(jfrogappsconfig.Module{SourceRoot: currWd}, []string{}) + err = applicabilityManager.deprecatedCreateConfigFile(jfrogappsconfig.Module{SourceRoot: currWd}, []string{}) assert.NoError(t, err) defer func() { @@ -190,6 +190,30 @@ func TestCreateConfigFile_VerifyFileWasCreated(t *testing.T) { assert.True(t, len(fileContent) > 0) } +func TestApplicabilityScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.Applicability.String(), 0) + require.NoError(t, err) + directCves, indirectCves := results.ExtractCvesFromScanResponse(jas.FakeBasicXrayResults, mockDirectDependencies) + applicabilityManager := newApplicabilityScanManager(directCves, indirectCves, scanner, false, ApplicabilityScannerType, scannerTempDir) + + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + assert.NoError(t, applicabilityManager.createConfigFileForTarget(results.ScanTarget{Target: currWd})) + + defer func() { + err = os.Remove(applicabilityManager.configFileName) + assert.NoError(t, err) + }() + + _, fileNotExistError := os.Stat(applicabilityManager.configFileName) + assert.NoError(t, fileNotExistError) + fileContent, err := os.ReadFile(applicabilityManager.configFileName) + assert.NoError(t, err) + assert.True(t, len(fileContent) > 0) +} + func TestParseResults_NewApplicabilityStatuses(t *testing.T) { testCases := []struct { name string @@ -236,7 +260,7 @@ func TestParseResults_NewApplicabilityStatuses(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { applicabilityManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", tc.fileName) - vulnerabilitiesResults, _, innerErr := jas.ReadJasScanRunsFromFile(applicabilityManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, applicabilityDocsUrlSuffix, scanner.MinSeverity) + vulnerabilitiesResults, _, innerErr := jas.ReadJasScanRunsFromFile(applicabilityManager.resultsFileName, applicabilityDocsUrlSuffix, scanner.MinSeverity, jfrogAppsConfigForTest.Modules[0].SourceRoot) if assert.NoError(t, innerErr) && assert.NotNil(t, vulnerabilitiesResults) { assert.Len(t, vulnerabilitiesResults, 1) assert.Len(t, vulnerabilitiesResults[0].Results, tc.expectedResults) diff --git a/jas/common.go b/jas/common.go index 9892c7e4b..4407197a7 100644 --- a/jas/common.go +++ b/jas/common.go @@ -12,7 +12,6 @@ import ( "unicode" "github.com/jfrog/gofrog/datastructures" - clientservices "github.com/jfrog/jfrog-client-go/xsc/services" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" "github.com/jfrog/jfrog-cli-core/v2/utils/config" @@ -30,6 +29,7 @@ import ( "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray" "github.com/jfrog/jfrog-client-go/xray/services" + xscServices "github.com/jfrog/jfrog-client-go/xsc/services" "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" "github.com/stretchr/testify/assert" "golang.org/x/exp/slices" @@ -50,17 +50,7 @@ type JasScanner struct { DiffMode bool ResultsToCompare *results.SecurityCommandResults Exclusions []string - // This field contains scanner specific exclude patterns from Config Profile - ScannersExclusions SpecificScannersExcludePatterns - MinSeverity severityutils.Severity -} - -type SpecificScannersExcludePatterns struct { - ContextualAnalysisExcludePatterns []string - SastExcludePatterns []string - SecretsExcludePatterns []string - IacExcludePatterns []string - MaliciousCodeExcludePatterns []string + MinSeverity severityutils.Severity } type JasScannerOption func(f *JasScanner) error @@ -102,9 +92,9 @@ func NewJasScanner(serverDetails *config.ServerDetails, options ...JasScannerOpt return } -func WithEnvVars(validateSecrets bool, diffMode JasDiffScanEnvValue, envVars map[string]string) JasScannerOption { +func WithEnvVars(diffMode JasDiffScanEnvValue, envVars map[string]string) JasScannerOption { return func(scanner *JasScanner) (err error) { - scanner.EnvVars, err = getJasEnvVars(scanner.ServerDetails, validateSecrets, diffMode, envVars) + scanner.EnvVars, err = getJasEnvVars(scanner.ServerDetails, diffMode, envVars) return } } @@ -130,27 +120,26 @@ func WithMinSeverity(minSeverity severityutils.Severity) JasScannerOption { } } -func getJasEnvVars(serverDetails *config.ServerDetails, validateSecrets bool, diffMode JasDiffScanEnvValue, vars map[string]string) (map[string]string, error) { +func getJasEnvVars(serverDetails *config.ServerDetails, diffMode JasDiffScanEnvValue, vars map[string]string) (map[string]string, error) { amBasicVars, err := GetAnalyzerManagerEnvVariables(serverDetails) if err != nil { return nil, err } - amBasicVars[JfSecretValidationEnvVariable] = strconv.FormatBool(validateSecrets) if diffMode != NotDiffScanEnvValue { amBasicVars[DiffScanEnvVariable] = string(diffMode) } return utils.MergeMaps(utils.ToEnvVarsMap(os.Environ()), amBasicVars, vars), nil } -func (js *JasScanner) GetResultsToCompareByRelativePath(relativeTarget string, technology techutils.Technology) (resultsToCompare *results.TargetResults) { - return results.SearchTargetResultsByRelativePath(relativeTarget, technology, js.ResultsToCompare) +func (js *JasScanner) GetResultsToCompareByRelativePath(relativeTarget string, technologies ...techutils.Technology) (resultsToCompare *results.TargetResults) { + return results.SearchTargetResultsByRelativePath(relativeTarget, js.ResultsToCompare, technologies...) } func CreateJFrogAppsConfig(workingDirs []string) (*jfrogappsconfig.JFrogAppsConfig, error) { if jfrogAppsConfig, err := jfrogappsconfig.LoadConfigIfExist(); err != nil { - log.Warn("Please note the 'jfrog-apps-config.yml' is soon to be deprecated. Please consider using flags, environment variables, or centrally via the JFrog platform.") return nil, errorutils.CheckError(err) } else if jfrogAppsConfig != nil { + log.Warn("Please note the 'jfrog-apps-config.yml' is soon to be deprecated. Please consider using flags, environment variables, or centrally via the JFrog platform.") // jfrog-apps-config.yml exist in the workspace for i := range jfrogAppsConfig.Modules { // converting to absolute path before starting the scan flow @@ -175,19 +164,29 @@ func CreateJFrogAppsConfig(workingDirs []string) (*jfrogappsconfig.JFrogAppsConf } type ScannerCmd interface { - Run(module jfrogappsconfig.Module) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) + DeprecatedRun(module jfrogappsconfig.Module, centralConfigExclusions []string) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) + Run(target results.ScanTarget) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) } -func (a *JasScanner) Run(scannerCmd ScannerCmd, module jfrogappsconfig.Module) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { - func() { - if vulnerabilitiesSarifRuns, violationsSarifRuns, err = scannerCmd.Run(module); err != nil { - return - } - }() +func (a *JasScanner) DeprecatedRun(scannerCmd ScannerCmd, module jfrogappsconfig.Module, centralConfigExclusions []string) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if vulnerabilitiesSarifRuns, violationsSarifRuns, err = scannerCmd.DeprecatedRun(module, centralConfigExclusions); err != nil { + return + } return } -func ReadJasScanRunsFromFile(fileName, wd, informationUrlSuffix string, minSeverity severityutils.Severity) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { +func (a *JasScanner) Run(scannerCmd ScannerCmd, target results.ScanTarget) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + return scannerCmd.Run(target) +} + +func GetRootsFromTarget(target results.ScanTarget) []string { + if len(target.Include) > 0 { + return target.Include + } + return []string{target.Target} +} + +func ReadJasScanRunsFromFile(fileName, informationUrlSuffix string, minSeverity severityutils.Severity, target string, includeDirs ...string) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { violationFileName := fmt.Sprintf("%s_violations.sarif", strings.TrimSuffix(fileName, ".sarif")) vulnFileExist, violationsFileExist, err := checkJasResultsFilesExist(fileName, violationFileName) if err != nil { @@ -198,13 +197,13 @@ func ReadJasScanRunsFromFile(fileName, wd, informationUrlSuffix string, minSever return } if vulnFileExist { - vulnerabilitiesSarifRuns, err = readJasScanRunsFromFile(fileName, wd, informationUrlSuffix, minSeverity) + vulnerabilitiesSarifRuns, err = readJasScanRunsFromFile(fileName, informationUrlSuffix, minSeverity, target, includeDirs...) if err != nil { return } } if violationsFileExist { - violationsSarifRuns, err = readJasScanRunsFromFile(violationFileName, wd, informationUrlSuffix, minSeverity) + violationsSarifRuns, err = readJasScanRunsFromFile(violationFileName, informationUrlSuffix, minSeverity, target, includeDirs...) } return } @@ -219,18 +218,18 @@ func checkJasResultsFilesExist(vulnFileName, violationsFileName string) (vulnFil return } -func readJasScanRunsFromFile(fileName, wd, informationUrlSuffix string, minSeverity severityutils.Severity) (sarifRuns []*sarif.Run, err error) { +func readJasScanRunsFromFile(fileName, informationUrlSuffix string, minSeverity severityutils.Severity, target string, includeDirs ...string) (sarifRuns []*sarif.Run, err error) { if sarifRuns, err = sarifutils.ReadScanRunsFromFile(fileName); err != nil { return } - processSarifRuns(sarifRuns, wd, informationUrlSuffix, minSeverity) + processSarifRuns(sarifRuns, informationUrlSuffix, minSeverity, target, includeDirs...) return } // This function processes the Sarif runs results: update invocations, fill missing information, exclude results and adding scores to rules -func processSarifRuns(sarifRuns []*sarif.Run, wd string, informationUrlSuffix string, minSeverity severityutils.Severity) { +func processSarifRuns(sarifRuns []*sarif.Run, informationUrlSuffix string, minSeverity severityutils.Severity, target string, includeDirs ...string) { for _, sarifRun := range sarifRuns { - fillMissingRequiredInvocationInformation(wd, sarifRun) + fillMissingRequiredInvocationInformation(sarifRun, target, includeDirs...) fillMissingRequiredDriverInformation(utils.BaseDocumentationURL+informationUrlSuffix, GetAnalyzerManagerVersion(), sarifRun) addScoreToRunRules(sarifRun) // Process results @@ -258,29 +257,15 @@ func isValidVersion(version string) bool { return unicode.IsDigit(firstChar) } -func fillMissingRequiredInvocationInformation(wd string, run *sarif.Run) { - // If no invocations are present, add an empty invocation with an empty working directory - if len(run.Invocations) == 0 { - run.Invocations = append(run.Invocations, sarif.NewInvocation().WithWorkingDirectory(sarif.NewArtifactLocation())) - } +func fillMissingRequiredInvocationInformation(run *sarif.Run, target string, includeDirs ...string) { + // Aggregate execution success across all existing invocations, then replace them with a single + // canonical invocation carrying the scan target as working directory (not the analyzerManager directory set by the scanner). Downstream consumers only + // use the working directory URI and the success flag, so no other invocation metadata is needed. + isExeSuccess := false for _, invocation := range run.Invocations { - // Set the actual working directory to the invocation, not the analyzerManager directory - // Also used to calculate relative paths if needed with it - invocation.WorkingDirectory.WithURI(utils.ToURI(wd)) - // Make sure the invocation not omitted attributes are set (the lib reports them as required but spec says they are optional) - if len(invocation.NotificationConfigurationOverrides) == 0 { - invocation.NotificationConfigurationOverrides = make([]*sarif.ConfigurationOverride, 0) - } - if len(invocation.RuleConfigurationOverrides) == 0 { - invocation.RuleConfigurationOverrides = make([]*sarif.ConfigurationOverride, 0) - } - if len(invocation.ToolConfigurationNotifications) == 0 { - invocation.ToolConfigurationNotifications = make([]*sarif.Notification, 0) - } - if len(invocation.ToolExecutionNotifications) == 0 { - invocation.ToolExecutionNotifications = make([]*sarif.Notification, 0) - } + isExeSuccess = isExeSuccess || (invocation.ExecutionSuccessful != nil && *invocation.ExecutionSuccessful) } + run.Invocations = []*sarif.Invocation{sarifutils.CreateNewInvocation(isExeSuccess, target, includeDirs...)} } func excludeSuppressResults(sarifResults []*sarif.Result) []*sarif.Result { @@ -376,7 +361,7 @@ var FakeBasicXrayResults = []services.ScanResponse{ } func InitJasTest(t *testing.T) (*JasScanner, func()) { - assert.NoError(t, DownloadAnalyzerManagerIfNeeded(0)) + assert.NoError(t, DownloadAnalyzerManagerIfNeeded("", nil, 0)) scanner, err := NewJasScanner(&FakeServerDetails) assert.NoError(t, err) return scanner, func() { @@ -389,6 +374,9 @@ func GetTestDataPath() string { } func GetModule(root string, appConfig *jfrogappsconfig.JFrogAppsConfig) *jfrogappsconfig.Module { + if appConfig == nil || len(appConfig.Modules) == 0 { + return nil + } for _, module := range appConfig.Modules { if module.SourceRoot == root { return &module @@ -397,10 +385,30 @@ func GetModule(root string, appConfig *jfrogappsconfig.JFrogAppsConfig) *jfrogap return nil } -func ShouldSkipScanner(module jfrogappsconfig.Module, scanType jasutils.JasScanType) bool { +func ShouldSkipScannerByConfigProfile(target results.ScanTarget, configProfile *xscServices.ConfigProfile, scanType utils.SubScanType, jasType jasutils.JasScanType) bool { + if configProfile == nil { + return false + } + log.Debug(fmt.Sprintf("Using config profile '%s' to determine whether to run %s scan...", configProfile.ProfileName, jasType)) + if centralConfiguredToRun := target.IsScanRequestedByCentralConfig(scanType); centralConfiguredToRun != nil && !*centralConfiguredToRun { + log.Debug(fmt.Sprintf("Skipping %s scan as not requested by '%s' config profile...", jasType, configProfile.ProfileName)) + return true + } + return false +} + +func ShouldSkipScannerByModule(target results.ScanTarget, scanType jasutils.JasScanType) bool { + if target.DeprecatedAppsConfigModule == nil { + return false + } lowerScanType := strings.ToLower(string(scanType)) - if slices.Contains(module.ExcludeScanners, lowerScanType) { - log.Info(fmt.Sprintf("Skipping %s scanning", scanType)) + if slices.Contains(target.DeprecatedAppsConfigModule.ExcludeScanners, lowerScanType) { + log.Debug(fmt.Sprintf("Skipping %s scan as requested by local module config...", scanType)) + return true + } + exclusions := target.GetDeprecatedAppsConfigModuleExclusions(scanType) + if len(exclusions) > 0 && utils.IsPathExcluded(target.Target, exclusions) { + log.Debug(fmt.Sprintf("Skipping %s scan as target is excluded by local module config...", scanType)) return true } return false @@ -421,7 +429,7 @@ func GetSourceRoots(module jfrogappsconfig.Module, scanner *jfrogappsconfig.Scan return roots, nil } -func GetExcludePatterns(module jfrogappsconfig.Module, scanner *jfrogappsconfig.Scanner, centralConfigExclusions []string, cliExclusions ...string) []string { +func GetJasExcludePatterns(module jfrogappsconfig.Module, scanner *jfrogappsconfig.Scanner, centralConfigExclusions []string, cliExclusions ...string) []string { uniqueExcludePatterns := datastructures.MakeSet[string]() if len(cliExclusions) > 0 || len(centralConfigExclusions) > 0 { // Adding exclusions from CLI requires to convert them to file exclude patterns @@ -441,6 +449,20 @@ func GetExcludePatterns(module jfrogappsconfig.Module, scanner *jfrogappsconfig. return uniqueExcludePatterns.ToSlice() } +func GetJasExcludePatternsForTarget(target results.ScanTarget, centralConfigExclusions []string) []string { + if len(centralConfigExclusions) > 0 { + return centralConfigExclusions + } + if utils.ElementsEqual(target.Exclude, utils.DefaultScaExcludePatterns) { + // Default SCA exclude patterns, so we use the default JAS exclude patterns + return utils.DefaultJasExcludePatterns + } + uniqueExcludePatterns := datastructures.MakeSet[string]() + // Adding exclude patterns from the CLI requires to convert them to file exclude patterns + uniqueExcludePatterns.AddElements(convertToFilesExcludePatterns(target.Exclude)...) + return uniqueExcludePatterns.ToSlice() +} + // This function convert every exclude pattern to a file exclude pattern form. // Checks are being made since some of the exclude patters we get here might already be in a file exclude pattern func convertToFilesExcludePatterns(excludePatterns []string) (converted []string) { @@ -515,19 +537,9 @@ func CreateScannerTempDirectory(scanner *JasScanner, scanType string, threadId i return scannerTempDir, nil } -func UpdateJasScannerWithExcludePatternsFromProfile(scanner *JasScanner, profile *clientservices.ConfigProfile) { - if profile == nil { - return - } - scanner.ScannersExclusions.ContextualAnalysisExcludePatterns = profile.Modules[0].ScanConfig.ContextualAnalysisScannerConfig.ExcludePatterns - scanner.ScannersExclusions.SastExcludePatterns = profile.Modules[0].ScanConfig.SastScannerConfig.ExcludePatterns - scanner.ScannersExclusions.SecretsExcludePatterns = profile.Modules[0].ScanConfig.SecretsScannerConfig.ExcludePatterns - scanner.ScannersExclusions.IacExcludePatterns = profile.Modules[0].ScanConfig.IacScannerConfig.ExcludePatterns -} - -func GetStartJasScanLog(scanType utils.SubScanType, threadId int, module jfrogappsconfig.Module, targetCount int) string { +func GetStartJasScanLog(scanType utils.SubScanType, threadId int, module *jfrogappsconfig.Module, targetCount int) string { outLog := goclientutils.GetLogMsgPrefix(threadId, false) + fmt.Sprintf("Running %s scan", scanType.ToTextString()) - if targetCount != 1 { + if targetCount != 1 && module != nil { outLog += fmt.Sprintf(" on target '%s'...", module.SourceRoot) } else { outLog += "..." diff --git a/jas/common_test.go b/jas/common_test.go index 31dbe7a1e..f2cb69ad6 100644 --- a/jas/common_test.go +++ b/jas/common_test.go @@ -15,6 +15,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" + xscServices "github.com/jfrog/jfrog-client-go/xsc/services" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" @@ -67,13 +68,20 @@ func TestCreateJFrogAppsConfigWithConfig(t *testing.T) { assert.Len(t, jfrogAppsConfig.Modules, 1) } -func TestShouldSkipScanner(t *testing.T) { +func TestShouldSkipScannerByModule(t *testing.T) { module := jfrogAppsConfig.Module{} - assert.False(t, ShouldSkipScanner(module, jasutils.IaC)) + assert.False(t, ShouldSkipScannerByModule(results.ScanTarget{DeprecatedAppsConfigModule: &module}, jasutils.IaC)) module = jfrogAppsConfig.Module{ExcludeScanners: []string{"sast"}} - assert.False(t, ShouldSkipScanner(module, jasutils.IaC)) - assert.True(t, ShouldSkipScanner(module, jasutils.Sast)) + assert.False(t, ShouldSkipScannerByModule(results.ScanTarget{DeprecatedAppsConfigModule: &module}, jasutils.IaC)) + assert.True(t, ShouldSkipScannerByModule(results.ScanTarget{DeprecatedAppsConfigModule: &module}, jasutils.Sast)) + + // no module + assert.False(t, ShouldSkipScannerByModule(results.ScanTarget{}, jasutils.IaC)) + assert.False(t, ShouldSkipScannerByModule(results.ScanTarget{}, jasutils.Sast)) + assert.False(t, ShouldSkipScannerByModule(results.ScanTarget{}, jasutils.Secrets)) + assert.False(t, ShouldSkipScannerByModule(results.ScanTarget{}, jasutils.MaliciousCode)) + assert.False(t, ShouldSkipScannerByModule(results.ScanTarget{}, jasutils.Applicability)) } var getSourceRootsCases = []struct { @@ -122,12 +130,12 @@ var getExcludePatternsCases = []struct { {&jfrogAppsConfig.Scanner{WorkingDirs: []string{"exclude-dir-1", "exclude-dir-2"}}}, } -func TestGetExcludePatterns(t *testing.T) { +func TestGetJasExcludePatterns(t *testing.T) { module := jfrogAppsConfig.Module{ExcludePatterns: []string{"exclude-root"}} for _, testCase := range getExcludePatternsCases { t.Run("", func(t *testing.T) { scanner := testCase.scanner - actualExcludePatterns := GetExcludePatterns(module, scanner, []string{}) + actualExcludePatterns := GetJasExcludePatterns(module, scanner, []string{}) if scanner == nil { assert.ElementsMatch(t, module.ExcludePatterns, actualExcludePatterns) return @@ -306,12 +314,11 @@ func TestConvertToFilesExcludePatterns(t *testing.T) { func TestGetJasEnvVars(t *testing.T) { tests := []struct { - name string - serverDetails *config.ServerDetails - validateSecrets bool - diffMode JasDiffScanEnvValue - extraEnvVars map[string]string - expectedOutput map[string]string + name string + serverDetails *config.ServerDetails + diffMode JasDiffScanEnvValue + extraEnvVars map[string]string + expectedOutput map[string]string }{ { name: "Valid server details", @@ -322,30 +329,27 @@ func TestGetJasEnvVars(t *testing.T) { AccessToken: "token", }, expectedOutput: map[string]string{ - jfPlatformUrlEnvVariable: "url", - jfUserEnvVariable: "user", - jfPasswordEnvVariable: "password", - jfTokenEnvVariable: "token", - JfSecretValidationEnvVariable: "false", + jfPlatformUrlEnvVariable: "url", + jfUserEnvVariable: "user", + jfPasswordEnvVariable: "password", + jfTokenEnvVariable: "token", }, }, { - name: "With validate secrets", + name: "With extra env vars", serverDetails: &config.ServerDetails{ Url: "url", User: "user", Password: "password", AccessToken: "token", }, - extraEnvVars: map[string]string{"test": "testValue"}, - validateSecrets: true, + extraEnvVars: map[string]string{"test": "testValue"}, expectedOutput: map[string]string{ - jfPlatformUrlEnvVariable: "url", - jfUserEnvVariable: "user", - jfPasswordEnvVariable: "password", - jfTokenEnvVariable: "token", - JfSecretValidationEnvVariable: "true", - "test": "testValue", + jfPlatformUrlEnvVariable: "url", + jfUserEnvVariable: "user", + jfPasswordEnvVariable: "password", + jfTokenEnvVariable: "token", + "test": "testValue", }, }, { @@ -392,18 +396,17 @@ func TestGetJasEnvVars(t *testing.T) { }, diffMode: FirstScanDiffScanEnvValue, expectedOutput: map[string]string{ - jfPlatformUrlEnvVariable: "url", - jfUserEnvVariable: "user", - jfPasswordEnvVariable: "password", - jfTokenEnvVariable: "token", - JfSecretValidationEnvVariable: "false", - DiffScanEnvVariable: string(FirstScanDiffScanEnvValue), + jfPlatformUrlEnvVariable: "url", + jfUserEnvVariable: "user", + jfPasswordEnvVariable: "password", + jfTokenEnvVariable: "token", + DiffScanEnvVariable: string(FirstScanDiffScanEnvValue), }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - envVars, err := getJasEnvVars(test.serverDetails, test.validateSecrets, test.diffMode, test.extraEnvVars) + envVars, err := getJasEnvVars(test.serverDetails, test.diffMode, test.extraEnvVars) assert.NoError(t, err) for expectedKey, expectedValue := range test.expectedOutput { assert.Equal(t, expectedValue, envVars[expectedKey]) @@ -579,11 +582,11 @@ func TestGetDiffScanTypeValue(t *testing.T) { func TestGetResultsToCompare(t *testing.T) { testCases := []struct { - name string - target string - compareTechnology techutils.Technology - ResultsToCompare *results.SecurityCommandResults - expectedTarget *results.TargetResults + name string + target string + compareTechs []techutils.Technology + ResultsToCompare *results.SecurityCommandResults + expectedTarget *results.TargetResults }{ { name: "No results to compare", @@ -624,23 +627,37 @@ func TestGetResultsToCompare(t *testing.T) { expectedTarget: &results.TargetResults{ScanTarget: results.ScanTarget{Target: filepath.Join("other", "root", "to", "target2")}}, }, { - name: "Results to compare - same directory two technologies picks npm", - target: filepath.Join("root", "app"), - compareTechnology: techutils.Npm, + name: "Results to compare - same directory two technologies picks npm", + target: filepath.Join("root", "app"), + compareTechs: []techutils.Technology{techutils.Npm}, + ResultsToCompare: &results.SecurityCommandResults{ + Targets: []*results.TargetResults{ + {ScanTarget: results.ScanTarget{Target: filepath.Join("root", "app"), Technologies: []techutils.Technology{techutils.Poetry}}}, + {ScanTarget: results.ScanTarget{Target: filepath.Join("root", "app"), Technologies: []techutils.Technology{techutils.Npm}}}, + }, + }, + expectedTarget: &results.TargetResults{ScanTarget: results.ScanTarget{Target: filepath.Join("root", "app"), Technologies: []techutils.Technology{techutils.Npm}}}, + }, + { + name: "Results to compare - same directory multi-technology target matches when technologies match", + target: filepath.Join("root", "mono"), + compareTechs: []techutils.Technology{ + techutils.Npm, techutils.Go, + }, ResultsToCompare: &results.SecurityCommandResults{ Targets: []*results.TargetResults{ - {ScanTarget: results.ScanTarget{Target: filepath.Join("root", "app"), Technology: techutils.Poetry}}, - {ScanTarget: results.ScanTarget{Target: filepath.Join("root", "app"), Technology: techutils.Npm}}, + {ScanTarget: results.ScanTarget{Target: filepath.Join("root", "mono"), Technologies: []techutils.Technology{techutils.Maven}}}, + {ScanTarget: results.ScanTarget{Target: filepath.Join("root", "mono"), Technologies: []techutils.Technology{techutils.Npm, techutils.Go}}}, }, }, - expectedTarget: &results.TargetResults{ScanTarget: results.ScanTarget{Target: filepath.Join("root", "app"), Technology: techutils.Npm}}, + expectedTarget: &results.TargetResults{ScanTarget: results.ScanTarget{Target: filepath.Join("root", "mono"), Technologies: []techutils.Technology{techutils.Npm, techutils.Go}}}, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { scanner := &JasScanner{ResultsToCompare: testCase.ResultsToCompare} - assert.Equal(t, testCase.expectedTarget, scanner.GetResultsToCompareByRelativePath(testCase.target, testCase.compareTechnology)) + assert.Equal(t, testCase.expectedTarget, scanner.GetResultsToCompareByRelativePath(testCase.target, testCase.compareTechs...)) }) } } @@ -660,7 +677,7 @@ func TestProcessSarifRuns(t *testing.T) { sarifutils.CreateResultWithOneLocation(fmt.Sprintf("file://%s", filepath.Join(wd, "dir", "file2")), 0, 0, 0, 0, "snippet", "rule1", "error"), )) - processSarifRuns(dummyReport.Runs, wd, "docs URL", severityutils.High) + processSarifRuns(dummyReport.Runs, "docs URL", severityutils.High, wd) run := dummyReport.Runs[0] // Check Invocation added. @@ -690,6 +707,109 @@ func TestProcessSarifRuns(t *testing.T) { require.Equal(t, "dir/file2", sarifutils.GetLocationFileName(result.Locations[0])) } +func TestShouldSkipScannerByConfigProfile(t *testing.T) { + enabledProfile := &xscServices.ConfigProfile{ + ProfileName: "test-profile", + Modules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + ScaScannerConfig: xscServices.ScaScannerConfig{EnableScaScan: true}, + ContextualAnalysisScannerConfig: xscServices.CaScannerConfig{EnableCaScan: true}, + IacScannerConfig: xscServices.IacScannerConfig{EnableIacScan: true}, + SecretsScannerConfig: xscServices.SecretsScannerConfig{EnableSecretsScan: true}, + SastScannerConfig: xscServices.SastScannerConfig{EnableSastScan: true}, + }, + }}, + } + + tests := []struct { + name string + target results.ScanTarget + profile *xscServices.ConfigProfile + scanType utils.SubScanType + jasType jasutils.JasScanType + expected bool + }{ + { + name: "Nil config profile - should not skip", + target: results.ScanTarget{Target: "/project"}, + profile: nil, + scanType: utils.SecretsScan, + jasType: jasutils.Secrets, + expected: false, + }, + { + name: "Scan enabled - should not skip", + target: results.ScanTarget{Target: "/project", CentralConfigModules: enabledProfile.Modules}, + profile: enabledProfile, + scanType: utils.SecretsScan, + jasType: jasutils.Secrets, + expected: false, + }, + { + name: "Scan disabled - should skip", + target: results.ScanTarget{Target: "/project", CentralConfigModules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + SecretsScannerConfig: xscServices.SecretsScannerConfig{EnableSecretsScan: false}, + }, + }}}, + profile: &xscServices.ConfigProfile{ + ProfileName: "disabled-profile", + Modules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + SecretsScannerConfig: xscServices.SecretsScannerConfig{EnableSecretsScan: false}, + }, + }}, + }, + scanType: utils.SecretsScan, + jasType: jasutils.Secrets, + expected: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, ShouldSkipScannerByConfigProfile(tt.target, tt.profile, tt.scanType, tt.jasType)) + }) + } +} + +func TestGetJasExcludePatternsForTarget(t *testing.T) { + tests := []struct { + name string + target results.ScanTarget + centralConfigExclusions []string + expected []string + }{ + { + name: "Central config exclusions take precedence", + target: results.ScanTarget{Target: "/project"}, + centralConfigExclusions: []string{"**/vendor/**", "**/generated/**"}, + expected: []string{"**/vendor/**", "**/generated/**"}, + }, + { + name: "Default SCA exclusions map to default JAS exclusions", + target: results.ScanTarget{Target: "/project", Exclude: utils.DefaultScaExcludePatterns}, + centralConfigExclusions: nil, + expected: utils.DefaultJasExcludePatterns, + }, + { + name: "Custom exclusions are converted to file exclude patterns", + target: results.ScanTarget{Target: "/project", Exclude: []string{"custom-dir"}}, + centralConfigExclusions: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetJasExcludePatternsForTarget(tt.target, tt.centralConfigExclusions) + if tt.expected != nil { + assert.ElementsMatch(t, tt.expected, result) + } else { + // For custom exclusions, just verify we got a non-empty result (converted patterns) + assert.NotEmpty(t, result) + } + }) + } +} + func TestIsAnySoftwareInstalled(t *testing.T) { tests := []struct { name string diff --git a/jas/iac/iacscanner.go b/jas/iac/iacscanner.go index c95911af3..05f802441 100644 --- a/jas/iac/iacscanner.go +++ b/jas/iac/iacscanner.go @@ -9,6 +9,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" @@ -28,28 +29,42 @@ type IacScanManager struct { resultsFileName string } +type IacScanParams struct { + ThreadId int + TargetCount int + ResultsToCompare []*sarif.Run + Target results.ScanTarget +} + // 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. -func RunIacScan(scanner *jas.JasScanner, module jfrogappsconfig.Module, targetCount, threadId int, resultsToCompare ...*sarif.Run) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { +func RunIacScan(scanner *jas.JasScanner, params IacScanParams) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { var scannerTempDir string - if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.IaC.String(), threadId); err != nil { + if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.IaC.String(), params.ThreadId); err != nil { return } - iacScanManager, err := newIacScanManager(scanner, scannerTempDir, resultsToCompare...) + iacScanManager, err := newIacScanManager(scanner, scannerTempDir, params.ResultsToCompare...) if err != nil { return } startTime := time.Now() - log.Info(jas.GetStartJasScanLog(utils.IacScan, threadId, module, targetCount)) - if vulnerabilitiesResults, violationsResults, err = iacScanManager.scanner.Run(iacScanManager, module); err != nil { + log.Info(jas.GetStartJasScanLog(utils.IacScan, params.ThreadId, params.Target.DeprecatedAppsConfigModule, params.TargetCount)) + if vulnerabilitiesResults, violationsResults, err = iacScanManager.runIacScan(params); err != nil { return } - log.Info(utils.GetScanFindingsLog(utils.IacScan, sarifutils.GetResultsLocationCount(vulnerabilitiesResults...), startTime, threadId)) + log.Info(utils.GetScanFindingsLog(utils.IacScan, sarifutils.GetResultsLocationCount(vulnerabilitiesResults...), startTime, params.ThreadId)) return } +func (iacScanManager *IacScanManager) runIacScan(params IacScanParams) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { + if params.Target.DeprecatedAppsConfigModule == nil { + return iacScanManager.scanner.Run(iacScanManager, params.Target) + } + return iacScanManager.scanner.DeprecatedRun(iacScanManager, *params.Target.DeprecatedAppsConfigModule, params.Target.GetCentralConfigExclusions(utils.IacScan)) +} + func newIacScanManager(scanner *jas.JasScanner, scannerTempDir string, resultsToCompare ...*sarif.Run) (manager *IacScanManager, err error) { manager = &IacScanManager{ scanner: scanner, @@ -69,14 +84,24 @@ func newIacScanManager(scanner *jas.JasScanner, scannerTempDir string, resultsTo return } -func (iac *IacScanManager) Run(module jfrogappsconfig.Module) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { - if err = iac.createConfigFile(module, iac.scanner.ScannersExclusions.IacExcludePatterns, iac.scanner.Exclusions...); err != nil { +func (iac *IacScanManager) DeprecatedRun(module jfrogappsconfig.Module, centralConfigExclusions []string) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = iac.deprecatedCreateConfigFile(module, centralConfigExclusions, iac.scanner.Exclusions...); err != nil { + return + } + if err = iac.runAnalyzerManager(); err != nil { + return + } + return jas.ReadJasScanRunsFromFile(iac.resultsFileName, iacDocsUrlSuffix, iac.scanner.MinSeverity, module.SourceRoot) +} + +func (iac *IacScanManager) Run(target results.ScanTarget) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = iac.createConfigFileForTarget(target); err != nil { return } if err = iac.runAnalyzerManager(); err != nil { return } - return jas.ReadJasScanRunsFromFile(iac.resultsFileName, module.SourceRoot, iacDocsUrlSuffix, iac.scanner.MinSeverity) + return jas.ReadJasScanRunsFromFile(iac.resultsFileName, iacDocsUrlSuffix, iac.scanner.MinSeverity, target.Target, target.Include...) } type iacScanConfig struct { @@ -91,7 +116,7 @@ type iacScanConfiguration struct { SkippedDirs []string `yaml:"skipped-folders"` } -func (iac *IacScanManager) createConfigFile(module jfrogappsconfig.Module, centralConfigExclusions []string, exclusions ...string) error { +func (iac *IacScanManager) deprecatedCreateConfigFile(module jfrogappsconfig.Module, centralConfigExclusions []string, exclusions ...string) error { roots, err := jas.GetSourceRoots(module, module.Scanners.Iac) if err != nil { return err @@ -103,7 +128,22 @@ func (iac *IacScanManager) createConfigFile(module jfrogappsconfig.Module, centr Output: iac.resultsFileName, PathToResultsToCompare: iac.resultsToCompareFileName, Type: iacScannerType, - SkippedDirs: jas.GetExcludePatterns(module, module.Scanners.Iac, centralConfigExclusions, exclusions...), + SkippedDirs: jas.GetJasExcludePatterns(module, module.Scanners.Iac, centralConfigExclusions, exclusions...), + }, + }, + } + return jas.CreateScannersConfigFile(iac.configFileName, configFileContent, jasutils.IaC) +} + +func (iac *IacScanManager) createConfigFileForTarget(target results.ScanTarget) error { + configFileContent := iacScanConfig{ + Scans: []iacScanConfiguration{ + { + Roots: jas.GetRootsFromTarget(target), + Output: iac.resultsFileName, + PathToResultsToCompare: iac.resultsToCompareFileName, + Type: iacScannerType, + SkippedDirs: jas.GetJasExcludePatternsForTarget(target, target.GetCentralConfigExclusions(utils.IacScan)), }, }, } diff --git a/jas/iac/iacscanner_test.go b/jas/iac/iacscanner_test.go index d07582d82..b23acd8e2 100644 --- a/jas/iac/iacscanner_test.go +++ b/jas/iac/iacscanner_test.go @@ -7,6 +7,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/stretchr/testify/require" @@ -56,18 +57,48 @@ func TestNewIacScanManagerWithFilesToCompare(t *testing.T) { assert.True(t, fileutils.IsPathExists(iacScanManager.resultsToCompareFileName, false)) } +func TestIacScan_CreateDeprecatedConfigFile_VerifyFileWasCreated(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.IaC.String(), 0) + require.NoError(t, err) + iacScanManager, err := newIacScanManager(scanner, scannerTempDir) + require.NoError(t, err) + + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + err = iacScanManager.deprecatedCreateConfigFile(jfrogappsconfig.Module{SourceRoot: currWd}, []string{}) + + defer func() { + err = os.Remove(iacScanManager.configFileName) + 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 TestIacScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() + tempDir, cleanUpTempDir := coreTests.CreateTempDirWithCallbackAndAssert(t) + defer cleanUpTempDir() + scanner.TempDir = tempDir scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.IaC.String(), 0) require.NoError(t, err) + iacScanManager, err := newIacScanManager(scanner, scannerTempDir) require.NoError(t, err) currWd, err := coreutils.GetWorkingDirectory() assert.NoError(t, err) - err = iacScanManager.createConfigFile(jfrogappsconfig.Module{SourceRoot: currWd}, []string{}) + err = iacScanManager.createConfigFileForTarget(results.ScanTarget{Target: currWd}) + assert.NoError(t, err) defer func() { err = os.Remove(iacScanManager.configFileName) @@ -93,7 +124,7 @@ func TestIacParseResults_EmptyResults(t *testing.T) { iacScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "iac-scan", "no-violations.sarif") // Act - vulnerabilitiesResults, violationResults, err := jas.ReadJasScanRunsFromFile(iacScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, iacDocsUrlSuffix, scanner.MinSeverity) + vulnerabilitiesResults, violationResults, err := jas.ReadJasScanRunsFromFile(iacScanManager.resultsFileName, iacDocsUrlSuffix, scanner.MinSeverity, jfrogAppsConfigForTest.Modules[0].SourceRoot) if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { assert.Len(t, vulnerabilitiesResults, 1) assert.Empty(t, vulnerabilitiesResults[0].Results) @@ -116,7 +147,7 @@ func TestIacParseResults_ResultsContainIacViolations(t *testing.T) { iacScanManager.resultsFileName = filepath.Join(tempDirPath, "contains-iac-issues.sarif") // Act - vulnerabilitiesResults, violationResults, err := jas.ReadJasScanRunsFromFile(iacScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, iacDocsUrlSuffix, scanner.MinSeverity) + vulnerabilitiesResults, violationResults, err := jas.ReadJasScanRunsFromFile(iacScanManager.resultsFileName, iacDocsUrlSuffix, scanner.MinSeverity, jfrogAppsConfigForTest.Modules[0].SourceRoot) if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { assert.Len(t, vulnerabilitiesResults, 1) assert.Len(t, vulnerabilitiesResults[0].Results, 4) diff --git a/jas/maliciouscode/maliciouscodescanner.go b/jas/maliciouscode/maliciouscodescanner.go index 7f2414d0a..ba5a07652 100644 --- a/jas/maliciouscode/maliciouscodescanner.go +++ b/jas/maliciouscode/maliciouscodescanner.go @@ -75,14 +75,14 @@ func newMaliciousScanManager(scanner *jas.JasScanner, scanType MaliciousScanType } func (mal *MaliciousScanManager) Run(sourceRoot string) (vulnerabilitiesSarifRuns []*sarif.Run, err error) { - if err = mal.createConfigFile(sourceRoot, append(mal.scanner.Exclusions, mal.scanner.ScannersExclusions.MaliciousCodeExcludePatterns...)...); err != nil { + if err = mal.createConfigFile(sourceRoot, mal.scanner.Exclusions...); err != nil { return } if err = mal.runAnalyzerManager(); err != nil { return } // Malicious code scans only return vulnerabilities, not violations - vulnerabilitiesSarifRuns, _, err = jas.ReadJasScanRunsFromFile(mal.resultsFileName, sourceRoot, malDocsUrlSuffix, mal.scanner.MinSeverity) + vulnerabilitiesSarifRuns, _, err = jas.ReadJasScanRunsFromFile(mal.resultsFileName, malDocsUrlSuffix, mal.scanner.MinSeverity, sourceRoot) return } diff --git a/jas/maliciouscode/maliciouscodescanner_test.go b/jas/maliciouscode/maliciouscodescanner_test.go index 82bcc338c..4671cd2b2 100644 --- a/jas/maliciouscode/maliciouscodescanner_test.go +++ b/jas/maliciouscode/maliciouscodescanner_test.go @@ -100,7 +100,7 @@ func TestParseResults_EmptyResults(t *testing.T) { maliciousScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "malicious-scan", "no-malicious.sarif") // Act - vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(maliciousScanManager.resultsFileName, currWd, malDocsUrlSuffix, scanner.MinSeverity) + vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(maliciousScanManager.resultsFileName, malDocsUrlSuffix, scanner.MinSeverity, currWd) // Assert if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { @@ -121,7 +121,7 @@ func TestParseResults_ResultsContainMalicious(t *testing.T) { maliciousScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "malicious-scan", "contain-malicious.sarif") // Act - vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(maliciousScanManager.resultsFileName, currWd, malDocsUrlSuffix, severityutils.Medium) + vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(maliciousScanManager.resultsFileName, malDocsUrlSuffix, severityutils.Medium, currWd) // Assert if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { diff --git a/jas/runner/jasrunner.go b/jas/runner/jasrunner.go index 0e8ec9d69..ab777371a 100644 --- a/jas/runner/jasrunner.go +++ b/jas/runner/jasrunner.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/jfrog/gofrog/parallel" - jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-security/jas" "github.com/jfrog/jfrog-cli-security/jas/applicability" @@ -26,9 +25,10 @@ type JasRunnerParams struct { Runner *utils.SecurityParallelRunner ServerDetails *config.ServerDetails Scanner *jas.JasScanner - // Module flags - Module jfrogappsconfig.Module + // Module / Target flags ConfigProfile *services.ConfigProfile + TargetCount int + ScanResults *results.TargetResults // Scan flags AllowPartialResults bool ScansToPerform []utils.SubScanType @@ -37,7 +37,8 @@ type JasRunnerParams struct { ChangedFiles []string DiffMode bool // Secret scan flags - SecretsScanType secrets.SecretsScanType + SecretsScanType secrets.SecretsScanType + SecretValidation bool // Contextual Analysis scan flags CvesProvider CveProvider ApplicableScanType applicability.ApplicabilityScanType @@ -47,20 +48,20 @@ type JasRunnerParams struct { SignedDescriptions bool SastRules string // Outputs - TargetCount int - ScanResults *results.TargetResults TargetOutputDir string } +func (params JasRunnerParams) runAllScanners() bool { + // For docker scan we support only secrets and contextual scans. + return params.ApplicableScanType == applicability.ApplicabilityScannerType || params.SecretsScanType == secrets.SecretsScannerType +} + // Cves are only available after the SCA scan is performed, so we need a provider to dynamically pass the discovered cves. type CveProvider func() (directCves []string, indirectCves []string) func AddJasScannersTasks(params JasRunnerParams) error { - // For docker scan we support only secrets and contextual scans. - runAllScanners := params.ApplicableScanType == applicability.ApplicabilityScannerType || params.SecretsScanType == secrets.SecretsScannerType - var errorsCollection error - if generalError := addJasScanTaskForModuleIfNeeded(params, utils.ContextualAnalysisScan, runContextualScan(¶ms)); generalError != nil { + if generalError := addJasScanTaskIfNeeded(params, utils.ContextualAnalysisScan, runContextualScan(¶ms)); generalError != nil { // Scan task addition failure should not impact the other scanners tasks addition, therefore we accumulate the errors and return the overall error at the end. errorsCollection = errors.Join(errorsCollection, generalError) } @@ -69,29 +70,29 @@ func AddJasScannersTasks(params JasRunnerParams) error { return errorsCollection } - if generalError := addJasScanTaskForModuleIfNeeded(params, utils.SecretsScan, runSecretsScan(¶ms)); generalError != nil { + if generalError := addJasScanTaskIfNeeded(params, utils.SecretsScan, runSecretsScan(¶ms)); generalError != nil { // Scan task addition failure should not impact the other scanners tasks addition, therefore we accumulate the errors and return the overall error at the end. errorsCollection = errors.Join(errorsCollection, generalError) } - if !runAllScanners { + if !params.runAllScanners() { // Binary scan only supports secrets and contextual scans. return errorsCollection } - if generalError := addJasScanTaskForModuleIfNeeded(params, utils.IacScan, runIacScan(¶ms)); generalError != nil { + if generalError := addJasScanTaskIfNeeded(params, utils.IacScan, runIacScan(¶ms)); generalError != nil { // Scan task addition failure should not impact the other scanners tasks addition, therefore we accumulate the errors and return the overall error at the end. errorsCollection = errors.Join(errorsCollection, generalError) } - if generalError := addJasScanTaskForModuleIfNeeded(params, utils.SastScan, runSastScan(¶ms)); generalError != nil { + if generalError := addJasScanTaskIfNeeded(params, utils.SastScan, runSastScan(¶ms)); generalError != nil { // Scan task addition failure should not impact the other scanners tasks addition, therefore we accumulate the errors and return the overall error at the end. errorsCollection = errors.Join(errorsCollection, generalError) } return errorsCollection } -func addJasScanTaskForModuleIfNeeded(params JasRunnerParams, subScan utils.SubScanType, task parallel.TaskFunc) (generalError error) { +func addJasScanTaskIfNeeded(params JasRunnerParams, subScan utils.SubScanType, task parallel.TaskFunc) (generalError error) { jasType := jasutils.SubScanTypeToJasScanType(subScan) if jasType == "" { return fmt.Errorf("failed to determine Jas scan type for %s", subScan) @@ -101,34 +102,19 @@ func addJasScanTaskForModuleIfNeeded(params JasRunnerParams, subScan utils.SubSc return } if params.ConfigProfile != nil { - log.Debug(fmt.Sprintf("Using config profile '%s' to determine whether to run %s scan...", params.ConfigProfile.ProfileName, jasType)) - enabled := false - switch jasType { - case jasutils.Secrets: - enabled = params.ConfigProfile.Modules[0].ScanConfig.SecretsScannerConfig.EnableSecretsScan - case jasutils.Sast: - enabled = params.ConfigProfile.Modules[0].ScanConfig.SastScannerConfig.EnableSastScan - case jasutils.IaC: - enabled = params.ConfigProfile.Modules[0].ScanConfig.IacScannerConfig.EnableIacScan - case jasutils.Applicability: - // In Applicability scanner we must check that Sca is also enabled, since we cannot run CA without Sca results - enabled = params.ConfigProfile.Modules[0].ScanConfig.ContextualAnalysisScannerConfig.EnableCaScan && params.ConfigProfile.Modules[0].ScanConfig.ScaScannerConfig.EnableScaScan + if jas.ShouldSkipScannerByConfigProfile(params.ScanResults.ScanTarget, params.ConfigProfile, subScan, jasType) { + return } - if enabled { - generalError = addModuleJasScanTask(jasType, params.Runner, task, params.ScanResults, params.AllowPartialResults) - } else { - log.Debug(fmt.Sprintf("Skipping %s scan as requested by '%s' config profile...", jasType, params.ConfigProfile.ProfileName)) - } - return + // If Config profile exists, we don't need to check for deprecated apps config module + return addJasScanTask(jasType, params.Runner, task, params.ScanResults, params.AllowPartialResults) } - if jas.ShouldSkipScanner(params.Module, jasType) { - log.Debug(fmt.Sprintf("Skipping %s scan as requested by local module config...", subScan)) + if jas.ShouldSkipScannerByModule(params.ScanResults.ScanTarget, jasType) { return } - return addModuleJasScanTask(jasType, params.Runner, task, params.ScanResults, params.AllowPartialResults) + return addJasScanTask(jasType, params.Runner, task, params.ScanResults, params.AllowPartialResults) } -func addModuleJasScanTask(scanType jasutils.JasScanType, securityParallelRunner *utils.SecurityParallelRunner, task parallel.TaskFunc, scanResults *results.TargetResults, allowSkippingErrors bool) (generalError error) { +func addJasScanTask(scanType jasutils.JasScanType, securityParallelRunner *utils.SecurityParallelRunner, task parallel.TaskFunc, scanResults *results.TargetResults, allowSkippingErrors bool) (generalError error) { securityParallelRunner.JasScannersWg.Add(1) if _, addTaskErr := securityParallelRunner.Runner.AddTaskWithError(task, func(err error) { _ = scanResults.AddTargetError(fmt.Errorf("failed to run %s scan: %s", scanType, err.Error()), allowSkippingErrors) @@ -143,7 +129,15 @@ func runSecretsScan(params *JasRunnerParams) parallel.TaskFunc { defer func() { params.Runner.JasScannersWg.Done() }() - vulnerabilitiesResults, violationsResults, err := secrets.RunSecretsScan(params.Scanner, params.SecretsScanType, params.Module, params.TargetCount, threadId, getSourceRunsToCompare(params, jasutils.Secrets)...) + secretsScanParams := secrets.SecretsScanParams{ + ThreadId: threadId, + TargetCount: params.TargetCount, + ScanType: params.SecretsScanType, + ValidateSecrets: params.SecretValidation, + ResultsToCompare: getSourceRunsToCompare(params, jasutils.Secrets), + Target: params.ScanResults.ScanTarget, + } + vulnerabilitiesResults, violationsResults, err := secrets.RunSecretsScan(params.Scanner, secretsScanParams) params.Runner.ResultsMu.Lock() defer params.Runner.ResultsMu.Unlock() // We first add the scan results and only then check for errors, so we can store the exit code in order to report it in the end @@ -160,7 +154,13 @@ func runIacScan(params *JasRunnerParams) parallel.TaskFunc { defer func() { params.Runner.JasScannersWg.Done() }() - vulnerabilitiesResults, violationsResults, err := iac.RunIacScan(params.Scanner, params.Module, params.TargetCount, threadId, getSourceRunsToCompare(params, jasutils.IaC)...) + iacScanParams := iac.IacScanParams{ + ThreadId: threadId, + TargetCount: params.TargetCount, + ResultsToCompare: getSourceRunsToCompare(params, jasutils.IaC), + Target: params.ScanResults.ScanTarget, + } + vulnerabilitiesResults, violationsResults, err := iac.RunIacScan(params.Scanner, iacScanParams) params.Runner.ResultsMu.Lock() defer params.Runner.ResultsMu.Unlock() // We first add the scan results and only then check for errors, so we can store the exit code in order to report it in the end @@ -179,7 +179,7 @@ func runSastScan(params *JasRunnerParams) parallel.TaskFunc { }() vulnerabilitiesResults, violationsResults, err := sast.RunSastScan( sast.SastScanParams{ - Module: params.Module, + Target: params.ScanResults.ScanTarget, SignedDescriptions: params.SignedDescriptions, SastRules: params.SastRules, TargetCount: params.TargetCount, @@ -219,7 +219,7 @@ func runContextualScan(params *JasRunnerParams) parallel.TaskFunc { ThirdPartyContextualAnalysis: params.ThirdPartyApplicabilityScan, ThreadId: threadId, TargetCount: params.TargetCount, - Module: params.Module, + Target: params.ScanResults.ScanTarget, }, params.Scanner, ) diff --git a/jas/runner/jasrunner_test.go b/jas/runner/jasrunner_test.go index 69e2f6c02..ce1e8d8ed 100644 --- a/jas/runner/jasrunner_test.go +++ b/jas/runner/jasrunner_test.go @@ -42,9 +42,9 @@ func TestJasRunner_AnalyzerManagerNotExist(t *testing.T) { func TestJasRunner(t *testing.T) { assert.NoError(t, testUtils.PrepareAnalyzerManagerResource()) securityParallelRunnerForTest := utils.CreateSecurityParallelRunner(cliutils.Threads) - targetResults := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true).SetSecretValidation(true).NewScanResults(results.ScanTarget{Target: "target", Technology: techutils.Pip}) + targetResults := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true).SetSecretValidation(true).NewScanResults(results.ScanTarget{Target: "target", Technologies: []techutils.Technology{techutils.Pip}}) - jasScanner, err := jas.NewJasScanner(&jas.FakeServerDetails, jas.WithEnvVars(false, jas.NotDiffScanEnvValue, jas.GetAnalyzerManagerXscEnvVars(false, "", "", "", "", []string{}, targetResults.GetTechnologies()...))) + jasScanner, err := jas.NewJasScanner(&jas.FakeServerDetails, jas.WithEnvVars(jas.NotDiffScanEnvValue, jas.GetAnalyzerManagerXscEnvVars(false, "", "", "", "", []string{}, targetResults.GetTechnologies()...))) assert.NoError(t, err) jasScanner.AnalyzerManager.AnalyzerManagerFullPath, err = jas.GetAnalyzerManagerExecutable() assert.NoError(t, err) @@ -65,7 +65,7 @@ func TestJasRunner(t *testing.T) { assert.NoError(t, AddJasScannersTasks(testParams)) } -func TestJasRunner_AnalyzerManagerReturnsError(t *testing.T) { +func TestJasRunner_Module_AnalyzerManagerReturnsError(t *testing.T) { assert.NoError(t, testUtils.PrepareAnalyzerManagerResource()) jfrogAppsConfigForTest, _ := jas.CreateJFrogAppsConfig(nil) @@ -76,7 +76,25 @@ func TestJasRunner_AnalyzerManagerReturnsError(t *testing.T) { DirectDependenciesCves: directCves, IndirectDependenciesCves: indirectCves, ScanType: applicability.ApplicabilityScannerType, - Module: jfrogAppsConfigForTest.Modules[0], + Target: results.ScanTarget{Target: "target", DeprecatedAppsConfigModule: &jfrogAppsConfigForTest.Modules[0]}, + }, + scanner, + ) + // Expect error: + assert.ErrorContains(t, jas.ParseAnalyzerManagerError(jasutils.Applicability, err), "failed to run Applicability scan") +} + +func TestJasRunner_Target_AnalyzerManagerReturnsError(t *testing.T) { + assert.NoError(t, testUtils.PrepareAnalyzerManagerResource()) + + scanner, _ := jas.NewJasScanner(&jas.FakeServerDetails) + directCves, indirectCves := results.ExtractCvesFromScanResponse(jas.FakeBasicXrayResults, []string{"issueId_2_direct_dependency", "issueId_1_direct_dependency"}) + _, err := applicability.RunApplicabilityScan( + applicability.ContextualAnalysisScanParams{ + DirectDependenciesCves: directCves, + IndirectDependenciesCves: indirectCves, + ScanType: applicability.ApplicabilityScannerType, + Target: results.ScanTarget{Target: "target"}, }, scanner, ) diff --git a/jas/sast/sastscanner.go b/jas/sast/sastscanner.go index 807b1630b..57db4619e 100644 --- a/jas/sast/sastscanner.go +++ b/jas/sast/sastscanner.go @@ -13,6 +13,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -42,10 +43,10 @@ type SastScanManager struct { } type SastScanParams struct { - Module jfrogappsconfig.Module SignedDescriptions bool SastRules string TargetCount int + Target results.ScanTarget ThreadId int SastChangedFiles []string ChangedFilesMode bool @@ -66,14 +67,21 @@ func RunSastScan(params SastScanParams, scanner *jas.JasScanner) (vulnerabilitie return } startTime := time.Now() - log.Info(jas.GetStartJasScanLog(utils.SastScan, params.ThreadId, params.Module, params.TargetCount)) - if vulnerabilitiesResults, violationsResults, err = sastScanManager.scanner.Run(sastScanManager, params.Module); err != nil { + log.Info(jas.GetStartJasScanLog(utils.SastScan, params.ThreadId, params.Target.DeprecatedAppsConfigModule, params.TargetCount)) + if vulnerabilitiesResults, violationsResults, err = sastScanManager.runSastScan(params); err != nil { return } log.Info(utils.GetScanFindingsLog(utils.SastScan, sarifutils.GetResultsLocationCount(vulnerabilitiesResults...), startTime, params.ThreadId)) return } +func (sastScanManager *SastScanManager) runSastScan(params SastScanParams) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { + if params.Target.DeprecatedAppsConfigModule == nil { + return sastScanManager.scanner.Run(sastScanManager, params.Target) + } + return sastScanManager.scanner.DeprecatedRun(sastScanManager, *params.Target.DeprecatedAppsConfigModule, params.Target.GetCentralConfigExclusions(utils.SastScan)) +} + func newSastScanManager(scanner *jas.JasScanner, scannerTempDir string, signedDescriptions, changedFilesMode bool, sastRules string, sastChangedFiles []string, resultsToCompare ...*sarif.Run) (manager *SastScanManager, err error) { manager = &SastScanManager{ scanner: scanner, @@ -95,14 +103,30 @@ func newSastScanManager(scanner *jas.JasScanner, scannerTempDir string, signedDe return } -func (ssm *SastScanManager) Run(module jfrogappsconfig.Module) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { - if err = ssm.createConfigFile(module, ssm.signedDescriptions, ssm.sastChangedFiles, ssm.scanner.ScannersExclusions.SastExcludePatterns, ssm.scanner.Exclusions...); err != nil { +func (ssm *SastScanManager) DeprecatedRun(module jfrogappsconfig.Module, centralConfigExclusions []string) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = ssm.deprecatedCreateConfigFile(module, ssm.signedDescriptions, ssm.sastChangedFiles, centralConfigExclusions, ssm.scanner.Exclusions...); err != nil { + return + } + if err = ssm.runAnalyzerManager(filepath.Dir(ssm.scanner.AnalyzerManager.AnalyzerManagerFullPath)); err != nil { + return + } + vulnerabilitiesSarifRuns, violationsSarifRuns, err = jas.ReadJasScanRunsFromFile(ssm.resultsFileName, sastDocsUrlSuffix, ssm.scanner.MinSeverity, module.SourceRoot) + if err != nil { + return + } + groupResultsByLocation(vulnerabilitiesSarifRuns) + groupResultsByLocation(violationsSarifRuns) + return +} + +func (ssm *SastScanManager) Run(target results.ScanTarget) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = ssm.createConfigFileForTarget(target); err != nil { return } if err = ssm.runAnalyzerManager(filepath.Dir(ssm.scanner.AnalyzerManager.AnalyzerManagerFullPath)); err != nil { return } - vulnerabilitiesSarifRuns, violationsSarifRuns, err = jas.ReadJasScanRunsFromFile(ssm.resultsFileName, module.SourceRoot, sastDocsUrlSuffix, ssm.scanner.MinSeverity) + vulnerabilitiesSarifRuns, violationsSarifRuns, err = jas.ReadJasScanRunsFromFile(ssm.resultsFileName, sastDocsUrlSuffix, ssm.scanner.MinSeverity, target.Target, target.Include...) if err != nil { return } @@ -131,7 +155,7 @@ type sastParameters struct { SignedDescriptions bool `yaml:"signed_descriptions,omitempty"` } -func (ssm *SastScanManager) createConfigFile(module jfrogappsconfig.Module, signedDescriptions bool, sastChangedFiles []string, centralConfigExclusions []string, exclusions ...string) error { +func (ssm *SastScanManager) deprecatedCreateConfigFile(module jfrogappsconfig.Module, signedDescriptions bool, sastChangedFiles []string, centralConfigExclusions []string, exclusions ...string) error { sastScanner := module.Scanners.Sast if sastScanner == nil { sastScanner = &jfrogappsconfig.SastScanner{} @@ -156,7 +180,26 @@ func (ssm *SastScanManager) createConfigFile(module jfrogappsconfig.Module, sign SastParameters: sastParameters{ SignedDescriptions: signedDescriptions, }, - ExcludePatterns: jas.GetExcludePatterns(module, &sastScanner.Scanner, centralConfigExclusions, exclusions...), + ExcludePatterns: jas.GetJasExcludePatterns(module, &sastScanner.Scanner, centralConfigExclusions, exclusions...), + UserRules: ssm.sastRules, + }, + }, + } + return jas.CreateScannersConfigFile(ssm.configFileName, configFileContent, jasutils.Sast) +} + +func (ssm *SastScanManager) createConfigFileForTarget(target results.ScanTarget) error { + configFileContent := sastScanConfig{ + Scans: []scanConfiguration{ + { + Type: sastScannerType, + Roots: jas.GetRootsFromTarget(target), + Output: ssm.resultsFileName, + PathToResultsToCompare: ssm.resultsToCompareFileName, + SastParameters: sastParameters{ + SignedDescriptions: ssm.signedDescriptions, + }, + ExcludePatterns: jas.GetJasExcludePatternsForTarget(target, target.GetCentralConfigExclusions(utils.SastScan)), UserRules: ssm.sastRules, }, }, diff --git a/jas/sast/sastscanner_test.go b/jas/sast/sastscanner_test.go index c09a25730..571cca0da 100644 --- a/jas/sast/sastscanner_test.go +++ b/jas/sast/sastscanner_test.go @@ -71,7 +71,7 @@ func TestSastParseResults_EmptyResults(t *testing.T) { sastScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "sast-scan", "no-violations.sarif") // Act - vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(sastScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, sastDocsUrlSuffix, scanner.MinSeverity) + vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(sastScanManager.resultsFileName, sastDocsUrlSuffix, scanner.MinSeverity, jfrogAppsConfigForTest.Modules[0].SourceRoot) // Assert if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { @@ -94,7 +94,7 @@ func TestSastParseResults_ResultsContainIacViolations(t *testing.T) { sastScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "sast-scan", "contains-sast-violations.sarif") // Act - vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(sastScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, sastDocsUrlSuffix, scanner.MinSeverity) + vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(sastScanManager.resultsFileName, sastDocsUrlSuffix, scanner.MinSeverity, jfrogAppsConfigForTest.Modules[0].SourceRoot) // Assert if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { @@ -336,7 +336,7 @@ func TestCreateConfigFile_ChangedFilesModeRoots(t *testing.T) { for _, tc := range []struct { name string changedFilesMode bool - // sastForCall is the slice passed to createConfigFile; nil to pass nil. + // sastForCall is the slice passed to deprecatedCreateConfigFile; nil to pass nil. sastForCall []string want []string emptyRoots bool @@ -369,7 +369,7 @@ func TestCreateConfigFile_ChangedFilesModeRoots(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { ssm.changedFilesMode = tc.changedFilesMode - require.NoError(t, ssm.createConfigFile(module, false, tc.sastForCall, nil)) + require.NoError(t, ssm.deprecatedCreateConfigFile(module, false, tc.sastForCall, nil)) got := readConfigRoots(t) if tc.emptyRoots { assert.Empty(t, got, "with changed-files mode on and no per-target list, roots should be nil/empty in YAML, not the default module source roots") diff --git a/jas/secrets/secretsscanner.go b/jas/secrets/secretsscanner.go index 34359edd2..f7591b088 100644 --- a/jas/secrets/secretsscanner.go +++ b/jas/secrets/secretsscanner.go @@ -2,6 +2,7 @@ package secrets import ( "path/filepath" + "strconv" "strings" "time" @@ -10,6 +11,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" ) @@ -29,37 +31,55 @@ type SecretScanManager struct { scanner *jas.JasScanner scanType SecretsScanType + validateSecrets bool resultsToCompareFileName string configFileName string resultsFileName string } +type SecretsScanParams struct { + ThreadId int + TargetCount int + ScanType SecretsScanType + ValidateSecrets bool + ResultsToCompare []*sarif.Run + Target results.ScanTarget +} + // 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. -func RunSecretsScan(scanner *jas.JasScanner, scanType SecretsScanType, module jfrogappsconfig.Module, targetCount, threadId int, resultsToCompare ...*sarif.Run) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { +func RunSecretsScan(scanner *jas.JasScanner, params SecretsScanParams) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { var scannerTempDir string - if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.Secrets.String(), threadId); err != nil { + if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.Secrets.String(), params.ThreadId); err != nil { return } - secretScanManager, err := newSecretsScanManager(scanner, scanType, scannerTempDir, resultsToCompare...) + secretScanManager, err := newSecretsScanManager(scanner, params.ScanType, params.ValidateSecrets, scannerTempDir, params.ResultsToCompare...) if err != nil { return } startTime := time.Now() - log.Info(jas.GetStartJasScanLog(utils.SecretsScan, threadId, module, targetCount)) - if vulnerabilitiesResults, violationsResults, err = secretScanManager.scanner.Run(secretScanManager, module); err != nil { + log.Info(jas.GetStartJasScanLog(utils.SecretsScan, params.ThreadId, params.Target.DeprecatedAppsConfigModule, params.TargetCount)) + if vulnerabilitiesResults, violationsResults, err = secretScanManager.runSecretsScan(params); err != nil { return } - log.Info(utils.GetScanFindingsLog(utils.SecretsScan, sarifutils.GetResultsLocationCount(vulnerabilitiesResults...), startTime, threadId)) + log.Info(utils.GetScanFindingsLog(utils.SecretsScan, sarifutils.GetResultsLocationCount(vulnerabilitiesResults...), startTime, params.ThreadId)) return } -func newSecretsScanManager(scanner *jas.JasScanner, scanType SecretsScanType, scannerTempDir string, resultsToCompare ...*sarif.Run) (manager *SecretScanManager, err error) { +func (secretScanManager *SecretScanManager) runSecretsScan(params SecretsScanParams) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { + if params.Target.DeprecatedAppsConfigModule == nil { + return secretScanManager.scanner.Run(secretScanManager, params.Target) + } + return secretScanManager.scanner.DeprecatedRun(secretScanManager, *params.Target.DeprecatedAppsConfigModule, params.Target.GetCentralConfigExclusions(utils.SecretsScan)) +} + +func newSecretsScanManager(scanner *jas.JasScanner, scanType SecretsScanType, validateSecrets bool, scannerTempDir string, resultsToCompare ...*sarif.Run) (manager *SecretScanManager, err error) { manager = &SecretScanManager{ scanner: scanner, scanType: scanType, + validateSecrets: validateSecrets, configFileName: filepath.Join(scannerTempDir, "config.yaml"), resultsFileName: filepath.Join(scannerTempDir, "results.sarif"), } @@ -74,14 +94,24 @@ func newSecretsScanManager(scanner *jas.JasScanner, scanType SecretsScanType, sc return } -func (ssm *SecretScanManager) Run(module jfrogappsconfig.Module) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { - if err = ssm.createConfigFile(module, ssm.scanner.ScannersExclusions.SecretsExcludePatterns, ssm.scanner.Exclusions...); err != nil { +func (ssm *SecretScanManager) DeprecatedRun(module jfrogappsconfig.Module, centralConfigExclusions []string) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = ssm.deprecatedCreateConfigFile(module, centralConfigExclusions, ssm.scanner.Exclusions...); err != nil { + return + } + if err = ssm.runAnalyzerManager(); err != nil { + return + } + return jas.ReadJasScanRunsFromFile(ssm.resultsFileName, secretsDocsUrlSuffix, ssm.scanner.MinSeverity, module.SourceRoot) +} + +func (ssm *SecretScanManager) Run(target results.ScanTarget) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = ssm.createConfigFileForTarget(target); err != nil { return } if err = ssm.runAnalyzerManager(); err != nil { return } - return jas.ReadJasScanRunsFromFile(ssm.resultsFileName, module.SourceRoot, secretsDocsUrlSuffix, ssm.scanner.MinSeverity) + return jas.ReadJasScanRunsFromFile(ssm.resultsFileName, secretsDocsUrlSuffix, ssm.scanner.MinSeverity, target.Target, target.Include...) } type secretsScanConfig struct { @@ -96,7 +126,7 @@ type secretsScanConfiguration struct { SkippedDirs []string `yaml:"skipped-folders"` } -func (s *SecretScanManager) createConfigFile(module jfrogappsconfig.Module, centralConfigExclusions []string, exclusions ...string) error { +func (s *SecretScanManager) deprecatedCreateConfigFile(module jfrogappsconfig.Module, centralConfigExclusions []string, exclusions ...string) error { roots, err := jas.GetSourceRoots(module, module.Scanners.Secrets) if err != nil { return err @@ -108,7 +138,22 @@ func (s *SecretScanManager) createConfigFile(module jfrogappsconfig.Module, cent Output: s.resultsFileName, PathToResultsToCompare: s.resultsToCompareFileName, Type: string(s.scanType), - SkippedDirs: jas.GetExcludePatterns(module, module.Scanners.Secrets, centralConfigExclusions, exclusions...), + SkippedDirs: jas.GetJasExcludePatterns(module, module.Scanners.Secrets, centralConfigExclusions, exclusions...), + }, + }, + } + return jas.CreateScannersConfigFile(s.configFileName, configFileContent, jasutils.Secrets) +} + +func (s *SecretScanManager) createConfigFileForTarget(target results.ScanTarget) error { + configFileContent := secretsScanConfig{ + Scans: []secretsScanConfiguration{ + { + Roots: jas.GetRootsFromTarget(target), + Output: s.resultsFileName, + PathToResultsToCompare: s.resultsToCompareFileName, + Type: string(s.scanType), + SkippedDirs: jas.GetJasExcludePatternsForTarget(target, target.GetCentralConfigExclusions(utils.SecretsScan)), }, }, } @@ -116,7 +161,9 @@ func (s *SecretScanManager) createConfigFile(module jfrogappsconfig.Module, cent } func (s *SecretScanManager) runAnalyzerManager() error { - return s.scanner.AnalyzerManager.Exec(s.configFileName, secretsScanCommand, filepath.Dir(s.scanner.AnalyzerManager.AnalyzerManagerFullPath), s.scanner.ServerDetails, s.scanner.EnvVars) + envVars := utils.MergeMaps(s.scanner.EnvVars) + envVars[jas.JfSecretValidationEnvVariable] = strconv.FormatBool(s.validateSecrets) + return s.scanner.AnalyzerManager.Exec(s.configFileName, secretsScanCommand, filepath.Dir(s.scanner.AnalyzerManager.AnalyzerManagerFullPath), s.scanner.ServerDetails, envVars) } func maskSecret(secret string) string { diff --git a/jas/secrets/secretsscanner_test.go b/jas/secrets/secretsscanner_test.go index 757b282c5..26f1669fd 100644 --- a/jas/secrets/secretsscanner_test.go +++ b/jas/secrets/secretsscanner_test.go @@ -7,6 +7,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/severityutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/stretchr/testify/require" @@ -22,7 +23,7 @@ import ( func TestNewSecretsScanManager(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() - secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, "temoDirPath") + secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, false, "temoDirPath") require.NoError(t, err) assert.NotEmpty(t, secretScanManager) @@ -43,7 +44,7 @@ func TestNewSecretsScanManagerWithFilesToCompare(t *testing.T) { scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.Secrets.String(), 0) require.NoError(t, err) - secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, scannerTempDir, sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyResult("test-markdown", "test-msg", "test-rule-id", "note"))) + secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, false, scannerTempDir, sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyResult("test-markdown", "test-msg", "test-rule-id", "note"))) require.NoError(t, err) // Check if path value exists and file is created @@ -51,18 +52,48 @@ func TestNewSecretsScanManagerWithFilesToCompare(t *testing.T) { assert.True(t, fileutils.IsPathExists(secretScanManager.resultsToCompareFileName, false)) } +func TestSecretsScan_CreateDeprecatedConfigFile_VerifyFileWasCreated(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.Secrets.String(), 0) + require.NoError(t, err) + secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, false, scannerTempDir) + require.NoError(t, err) + + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + err = secretScanManager.deprecatedCreateConfigFile(jfrogappsconfig.Module{SourceRoot: currWd}, []string{}) + assert.NoError(t, err) + + defer func() { + err = os.Remove(secretScanManager.configFileName) + 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 TestSecretsScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() + tempDir, cleanUpTempDir := coreTests.CreateTempDirWithCallbackAndAssert(t) + defer cleanUpTempDir() + scanner.TempDir = tempDir scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.Secrets.String(), 0) require.NoError(t, err) - secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, scannerTempDir) + + secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, false, scannerTempDir) require.NoError(t, err) currWd, err := coreutils.GetWorkingDirectory() assert.NoError(t, err) - err = secretScanManager.createConfigFile(jfrogappsconfig.Module{SourceRoot: currWd}, []string{}) + err = secretScanManager.createConfigFileForTarget(results.ScanTarget{Target: currWd}) assert.NoError(t, err) defer func() { @@ -85,7 +116,7 @@ func TestRunAnalyzerManager_ReturnsGeneralError(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() - secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, "temoDirPath") + secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, false, "temoDirPath") require.NoError(t, err) assert.Error(t, secretScanManager.runAnalyzerManager()) } @@ -96,12 +127,12 @@ func TestParseResults_EmptyResults(t *testing.T) { jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) assert.NoError(t, err) // Arrange - secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, "temoDirPath") + secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, false, "temoDirPath") require.NoError(t, err) secretScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "secrets-scan", "no-secrets.sarif") // Act - vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(secretScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, secretsDocsUrlSuffix, scanner.MinSeverity) + vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(secretScanManager.resultsFileName, secretsDocsUrlSuffix, scanner.MinSeverity, jfrogAppsConfigForTest.Modules[0].SourceRoot) // Assert if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { @@ -121,12 +152,12 @@ func TestParseResults_ResultsContainSecrets(t *testing.T) { jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) assert.NoError(t, err) - secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, "temoDirPath") + secretScanManager, err := newSecretsScanManager(scanner, SecretsScannerType, false, "temoDirPath") require.NoError(t, err) secretScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "secrets-scan", "contain-secrets.sarif") // Act - vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(secretScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, secretsDocsUrlSuffix, severityutils.Medium) + vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(secretScanManager.resultsFileName, secretsDocsUrlSuffix, severityutils.Medium, jfrogAppsConfigForTest.Modules[0].SourceRoot) // Assert if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { @@ -145,7 +176,12 @@ func TestGetSecretsScanResults_AnalyzerManagerReturnsError(t *testing.T) { defer cleanUp() jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) assert.NoError(t, err) - vulnerabilitiesResults, _, err := RunSecretsScan(scanner, SecretsScannerType, jfrogAppsConfigForTest.Modules[0], 1, 0) + secretsScanParams := SecretsScanParams{ + TargetCount: 1, + ScanType: SecretsScannerType, + Target: results.ScanTarget{Target: jfrogAppsConfigForTest.Modules[0].SourceRoot, DeprecatedAppsConfigModule: &jfrogAppsConfigForTest.Modules[0]}, + } + vulnerabilitiesResults, _, err := RunSecretsScan(scanner, secretsScanParams) assert.Error(t, err) assert.ErrorContains(t, jas.ParseAnalyzerManagerError(jasutils.Secrets, err), "failed to run Secrets scan") assert.Nil(t, vulnerabilitiesResults) diff --git a/sca/bom/bomgenerator.go b/sca/bom/bomgenerator.go index 4d022fb2c..57214a4de 100644 --- a/sca/bom/bomgenerator.go +++ b/sca/bom/bomgenerator.go @@ -2,6 +2,7 @@ package bom import ( "fmt" + "time" "github.com/CycloneDX/cyclonedx-go" @@ -28,6 +29,7 @@ type SbomGeneratorOption func(sg SbomGenerator) type SbomGeneratorParams struct { Target *results.TargetResults + TotalTargets int AllowPartialResults bool ScanResultsOutputDir string @@ -36,6 +38,12 @@ type SbomGeneratorParams struct { } func GenerateSbomForTarget(generator SbomGenerator, params SbomGeneratorParams) { + startLog := "Generating SBOM" + if params.TotalTargets > 1 { + startLog += fmt.Sprintf(" for target: %s", params.Target.Target) + } + log.Info(startLog + "...") + startTime := time.Now() // Generate the SBOM for the target sbom, err := generator.GenerateSbom(params.Target.ScanTarget) if err != nil { @@ -47,6 +55,9 @@ func GenerateSbomForTarget(generator SbomGenerator, params SbomGeneratorParams) // If in diff mode, get the diff SBOM compared to the previous target result sbom = getDiffSbom(sbom, params) } + if err := logLibComponents(sbom, params.TotalTargets, params.Target.Target, startTime); err != nil { + log.Warn(fmt.Sprintf("Failed to log library components in SBOM for %s: %s", params.Target.Target, err.Error())) + } // Set the SBOM in the target results and update target information updateTarget(params.Target, sbom) // Save the SBOM to a file @@ -75,9 +86,6 @@ func getDiffSbom(sbom *cyclonedx.BOM, params SbomGeneratorParams) *cyclonedx.BOM func updateTarget(target *results.TargetResults, sbom *cyclonedx.BOM) { target.SetSbom(sbom) target.ResultsStatus.UpdateStatus(results.CmdStepSbom, utils.NewIntPtr(0)) - if err := logLibComponents(sbom.Components); err != nil { - log.Warn(fmt.Sprintf("Failed to log library components in SBOM for %s: %s", target.Target, err.Error())) - } if target.Name != "" { // Target name is already set, no need to update. return @@ -104,21 +112,28 @@ func updateTarget(target *results.TargetResults, sbom *cyclonedx.BOM) { target.Name, _, _ = techutils.SplitPackageURL(rootComponent.PackageURL) } -func logLibComponents(components *[]cyclonedx.Component) (err error) { - if log.GetLogger().GetLogLevel() != log.DEBUG { - // Avoid printing and marshaling if not on DEBUG mode. - return - } +func logLibComponents(output *cyclonedx.BOM, totalTargets int, target string, startTime time.Time) (err error) { libs := []string{} - if components != nil { - for _, component := range *components { + if output != nil && output.Components != nil { + for _, component := range *output.Components { if component.Type == cyclonedx.ComponentTypeLibrary { libs = append(libs, techutils.PurlToXrayComponentId(component.PackageURL)) } } } + outLog := "SBOM generated" + if totalTargets > 1 { + outLog += fmt.Sprintf(" for target '%s'", target) + } + outLog += ";" if len(libs) == 0 { - log.Debug("No library components found.") + outLog += " no library components were found" + } else { + outLog += fmt.Sprintf(" found %d library components", len(libs)) + } + log.Info(fmt.Sprintf("%s (duration %s)", outLog, time.Since(startTime).String())) + if log.GetLogger().GetLogLevel() != log.DEBUG || len(libs) == 0 { + // Avoid printing and marshaling if not on DEBUG mode or no library components found. return } // Log the unique library components in the SBOM. diff --git a/sca/bom/buildinfo/buildinfobom.go b/sca/bom/buildinfo/buildinfobom.go index 53c0ae235..a8d504812 100644 --- a/sca/bom/buildinfo/buildinfobom.go +++ b/sca/bom/buildinfo/buildinfobom.go @@ -102,11 +102,10 @@ func (b *BuildInfoBomGenerator) GenerateSbom(target results.ScanTarget) (sbom *c generalError = errors.Join(generalError, errorutils.CheckError(os.Chdir(currentWorkingDir))) }() } - if target.Technology == techutils.NoTech { + if len(target.DetectedTechnologies()) == 0 { log.Debug(fmt.Sprintf("Couldn't determine a package manager or build tool used by '%s'.", target.Target)) return } - log.Debug(fmt.Sprintf("Generating '%s' dependency tree...", target.Target)) treeResult, bdtErr := b.buildDependencyTree(target) if bdtErr != nil { var projectNotInstalledErr *biUtils.ErrProjectNotInstalled @@ -147,18 +146,87 @@ func (b *BuildInfoBomGenerator) buildDependencyTree(scan results.ScanTarget) (*D if err := os.Chdir(scan.Target); err != nil { return nil, errorutils.CheckError(err) } - serverDetails, err := SetResolutionRepoInParamsIfExists(&b.params, scan.Technology) - if err != nil { - return nil, err + techs := scan.DetectedTechnologies() + if len(techs) == 0 { + return nil, errorutils.CheckErrorf("no technologies found for target '%s'", scan.Target) } - treeResult, techErr := GetTechDependencyTree(b.params, serverDetails, scan.Technology) - if techErr != nil { - return nil, fmt.Errorf("failed while building '%s' dependency tree: %w", scan.Technology, techErr) + merged := &DependencyTreeResult{DownloadUrls: map[string]string{}} + var buildErr error + hasAnyTree := false + for _, tech := range techs { + log.Debug(fmt.Sprintf("Generating '%s' dependency tree for '%s'...", tech.ToFormal(), scan.Target)) + serverDetails, err := SetResolutionRepoInParamsIfExists(&b.params, tech) + if err != nil { + buildErr = errors.Join(buildErr, fmt.Errorf("failed to set resolution repo in params: %w", err)) + continue + } + treeResult, err := GetTechDependencyTree(b.params, serverDetails, tech) + if err != nil { + buildErr = errors.Join(buildErr, fmt.Errorf("failed while building '%s' dependency tree: %w", tech, err)) + continue + } + if treeResult.FlatTree == nil || len(treeResult.FlatTree.Nodes) == 0 { + log.Debug(fmt.Sprintf("No dependencies found for '%s' in '%s'", tech, scan.Target)) + continue + } + hasAnyTree = true + merged = mergeResults(merged, &treeResult) } - if treeResult.FlatTree == nil || len(treeResult.FlatTree.Nodes) == 0 { + if !hasAnyTree { + if buildErr != nil { + return nil, buildErr + } return nil, errorutils.CheckErrorf("no dependencies were found. Please try to build your project and re-run the audit command") } - return &treeResult, nil + return merged, buildErr +} + +func mergeResults(existing, additional *DependencyTreeResult) *DependencyTreeResult { + if additional == nil { + return existing + } + if existing == nil { + return additional + } + for k, v := range additional.DownloadUrls { + if _, exists := existing.DownloadUrls[k]; !exists { + existing.DownloadUrls[k] = v + } else { + log.Warn(fmt.Sprintf("Download URL '%s' already exists in the existing dependency tree, skipping additional download URL '%s'.", k, v)) + } + } + existing.FullDepTrees = append(existing.FullDepTrees, additional.FullDepTrees...) + existing.FlatTree = mergeFlatTrees(existing.FlatTree, additional.FlatTree) + return existing +} + +// existing.FlatTree generated by buildDependencyTree is single node with 'root' ID and all the components as its nodes. +// We need to merge the additional flat tree into the existing flat tree. +func mergeFlatTrees(existing, additional *xrayUtils.GraphNode) *xrayUtils.GraphNode { + if additional == nil || len(additional.Nodes) == 0 { + if existing == nil || len(existing.Nodes) == 0 { + return nil + } + return existing + } + if existing == nil { + return additional + } + seen := datastructures.MakeSet[string]() + for _, node := range existing.Nodes { + seen.Add(node.Id) + } + for _, node := range additional.Nodes { + if seen.Exists(node.Id) { + continue + } + seen.Add(node.Id) + existing.Nodes = append(existing.Nodes, node) + } + if len(existing.Nodes) == 0 { + return nil + } + return existing } type DependencyTreeResult struct { @@ -278,7 +346,7 @@ func getCurationCacheFolderAndLogMsg(params technologies.BuildInfoBomGeneratorPa func getCurationCacheByTech(tech techutils.Technology) (string, error) { if tech == techutils.Maven || tech == techutils.Go { - return utils.GetCurationCacheFolderByTech(tech) + return utils.GetCurationCacheFolderByTech(tech.String()) } return "", nil } diff --git a/sca/bom/buildinfo/buildinfobom_test.go b/sca/bom/buildinfo/buildinfobom_test.go index 6243aa9d3..5c64592af 100644 --- a/sca/bom/buildinfo/buildinfobom_test.go +++ b/sca/bom/buildinfo/buildinfobom_test.go @@ -12,6 +12,148 @@ import ( "github.com/stretchr/testify/require" ) +func TestMergeResults(t *testing.T) { + nodeA := &xrayUtils.GraphNode{Id: "npm://a:1"} + nodeB := &xrayUtils.GraphNode{Id: "gav://b:2"} + nodeC := &xrayUtils.GraphNode{Id: "pypi://c:3"} + fullTreeA := &xrayUtils.GraphNode{Id: "root-a"} + fullTreeB := &xrayUtils.GraphNode{Id: "root-b"} + + testCases := []struct { + name string + existing *DependencyTreeResult + additional *DependencyTreeResult + assertFn func(t *testing.T, got, existing *DependencyTreeResult) + }{ + { + name: "nil additional returns existing unchanged", + existing: &DependencyTreeResult{ + FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{nodeA}}, + DownloadUrls: map[string]string{"pkg:a": "https://a"}, + }, + additional: nil, + assertFn: func(t *testing.T, got, existing *DependencyTreeResult) { + assert.Same(t, existing, got) + assert.Len(t, got.FlatTree.Nodes, 1) + assert.Equal(t, "https://a", got.DownloadUrls["pkg:a"]) + }, + }, + { + name: "nil existing returns additional", + existing: nil, + additional: &DependencyTreeResult{FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{nodeB}}}, + assertFn: func(t *testing.T, got, _ *DependencyTreeResult) { + require.NotNil(t, got) + assert.Len(t, got.FlatTree.Nodes, 1) + assert.Equal(t, "gav://b:2", got.FlatTree.Nodes[0].Id) + }, + }, + { + name: "merges flat tree nodes without duplicates", + existing: &DependencyTreeResult{ + FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{nodeA, nodeB}}, + DownloadUrls: map[string]string{}, + }, + additional: &DependencyTreeResult{ + FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{nodeB, nodeC}}, + }, + assertFn: func(t *testing.T, got, _ *DependencyTreeResult) { + require.NotNil(t, got.FlatTree) + assert.ElementsMatch(t, []*xrayUtils.GraphNode{nodeA, nodeB, nodeC}, got.FlatTree.Nodes) + }, + }, + { + name: "merging same nodes again does not duplicate", + existing: &DependencyTreeResult{ + FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{nodeA, nodeB}}, + DownloadUrls: map[string]string{}, + }, + additional: &DependencyTreeResult{ + FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{nodeA}}, + }, + assertFn: func(t *testing.T, got, _ *DependencyTreeResult) { + assert.Len(t, got.FlatTree.Nodes, 2) + }, + }, + { + name: "merges download URLs without overwriting existing keys", + existing: &DependencyTreeResult{ + FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{nodeA}}, + DownloadUrls: map[string]string{"pkg:a": "https://existing"}, + }, + additional: &DependencyTreeResult{ + FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{nodeC}}, + DownloadUrls: map[string]string{"pkg:a": "https://new", "pkg:c": "https://c"}, + }, + assertFn: func(t *testing.T, got, _ *DependencyTreeResult) { + assert.Equal(t, "https://existing", got.DownloadUrls["pkg:a"]) + assert.Equal(t, "https://c", got.DownloadUrls["pkg:c"]) + }, + }, + { + name: "appends full dependency trees", + existing: &DependencyTreeResult{ + FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{nodeA}}, + FullDepTrees: []*xrayUtils.GraphNode{fullTreeA}, + DownloadUrls: map[string]string{}, + }, + additional: &DependencyTreeResult{ + FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{nodeB}}, + FullDepTrees: []*xrayUtils.GraphNode{fullTreeB}, + }, + assertFn: func(t *testing.T, got, _ *DependencyTreeResult) { + assert.Equal(t, []*xrayUtils.GraphNode{fullTreeA, fullTreeB}, got.FullDepTrees) + }, + }, + { + name: "clears flat tree when no nodes remain", + existing: &DependencyTreeResult{ + FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{}}, + DownloadUrls: map[string]string{}, + }, + additional: &DependencyTreeResult{ + FlatTree: &xrayUtils.GraphNode{Nodes: []*xrayUtils.GraphNode{}}, + }, + assertFn: func(t *testing.T, got, _ *DependencyTreeResult) { + assert.Nil(t, got.FlatTree) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var existing, additional *DependencyTreeResult + if tc.existing != nil { + existing = copyDependencyTreeResult(tc.existing) + } + if tc.additional != nil { + additional = copyDependencyTreeResult(tc.additional) + } + got := mergeResults(existing, additional) + tc.assertFn(t, got, existing) + }) + } +} + +func copyDependencyTreeResult(src *DependencyTreeResult) *DependencyTreeResult { + if src == nil { + return nil + } + dst := &DependencyTreeResult{ + FullDepTrees: append([]*xrayUtils.GraphNode(nil), src.FullDepTrees...), + DownloadUrls: make(map[string]string, len(src.DownloadUrls)), + } + for k, v := range src.DownloadUrls { + dst.DownloadUrls[k] = v + } + if src.FlatTree != nil { + nodes := make([]*xrayUtils.GraphNode, len(src.FlatTree.Nodes)) + copy(nodes, src.FlatTree.Nodes) + dst.FlatTree = &xrayUtils.GraphNode{Nodes: nodes} + } + return dst +} + func TestGetDiffDependencyTree(t *testing.T) { targetResults := &results.TargetResults{ ScanTarget: results.ScanTarget{Target: "targetPath"}, diff --git a/sca/bom/buildinfo/technologies/common.go b/sca/bom/buildinfo/technologies/common.go index 56978e5a4..6e2b3f002 100644 --- a/sca/bom/buildinfo/technologies/common.go +++ b/sca/bom/buildinfo/technologies/common.go @@ -83,11 +83,10 @@ func (bbp *BuildInfoBomGeneratorParams) SetConanProfile(file string) *BuildInfoB return bbp } -func GetExcludePattern(configProfile *xscservices.ConfigProfile, isRecursive bool, exclusions ...string) string { +func GetScaExcludePattern(configProfile *xscservices.ConfigProfile, isRecursive bool, exclusions ...string) string { if configProfile != nil { exclusions = append(exclusions, configProfile.Modules[0].ScanConfig.ScaScannerConfig.ExcludePatterns...) } - if len(exclusions) == 0 { exclusions = append(exclusions, utils.DefaultScaExcludePatterns...) } diff --git a/sca/bom/buildinfo/technologies/common_test.go b/sca/bom/buildinfo/technologies/common_test.go index 268afa683..b9241384e 100644 --- a/sca/bom/buildinfo/technologies/common_test.go +++ b/sca/bom/buildinfo/technologies/common_test.go @@ -17,7 +17,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetExcludePattern(t *testing.T) { +func TestGetScaExcludePattern(t *testing.T) { tests := []struct { name string exclusions []string @@ -69,7 +69,7 @@ func TestGetExcludePattern(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - result := GetExcludePattern(test.configProfile, test.isRecursiveScan, test.exclusions...) + result := GetScaExcludePattern(test.configProfile, test.isRecursiveScan, test.exclusions...) assert.Equal(t, test.expected, result) }) } diff --git a/sca/bom/buildinfo/technologies/go/golang.go b/sca/bom/buildinfo/technologies/go/golang.go index 59cbe9431..6626dd554 100644 --- a/sca/bom/buildinfo/technologies/go/golang.go +++ b/sca/bom/buildinfo/technologies/go/golang.go @@ -33,7 +33,7 @@ func BuildDependencyTree(params technologies.BuildInfoBomGeneratorParams) (depen if params.IsCurationCmd { goProxyParams.EndpointPrefix = coreutils.CurationPassThroughApi goProxyParams.Direct = false - projCacheDir, errCacheFolder := utils.GetCurationCacheFolderByTech(techutils.Go) + projCacheDir, errCacheFolder := utils.GetCurationCacheFolderByTech(techutils.Go.String()) if errCacheFolder != nil { err = errCacheFolder return diff --git a/sca/bom/xrayplugin/plugin/config.go b/sca/bom/xrayplugin/plugin/config.go index b21bc0003..e1fc6f77d 100644 --- a/sca/bom/xrayplugin/plugin/config.go +++ b/sca/bom/xrayplugin/plugin/config.go @@ -20,4 +20,6 @@ type Config struct { IgnorePatterns []string `json:"ignorePatterns,omitempty" yaml:"ignorePatterns,omitempty"` // [Optional] Ecosystems to scan. Ecosystems []techutils.Technology `json:"ecosystems,omitempty" yaml:"ecosystems,omitempty"` + // [Optional] Specific directories to scan. + IncludeDirs []string `json:"includeDirs,omitempty"` } diff --git a/sca/bom/xrayplugin/plugin/plugin.go b/sca/bom/xrayplugin/plugin/plugin.go index d57cee065..cf007c276 100644 --- a/sca/bom/xrayplugin/plugin/plugin.go +++ b/sca/bom/xrayplugin/plugin/plugin.go @@ -189,7 +189,7 @@ func (p *Plugin) Client(broker *goplugin.MuxBroker, client *rpc.Client) (any, er return &ScannerRPCClient{client: client}, nil } -func DownloadXrayLibPluginIfNeeded() error { +func DownloadXrayLibPluginIfNeeded(remoteRepo string, remoteServerDetails *config.ServerDetails) error { downloadPath, err := GetXrayLibPluginDownloadPath() if err != nil { return err @@ -198,7 +198,7 @@ func DownloadXrayLibPluginIfNeeded() error { if err != nil { return err } - return utils.DownloadResourceFromPlatformIfNeeded("Xray-Lib Plugin", downloadPath, xrayLibPluginDirPath, path.Base(downloadPath), true, 0) + return utils.DownloadResourceFromPlatformIfNeeded("Xray-Lib Plugin", downloadPath, xrayLibPluginDirPath, path.Base(downloadPath), true, remoteRepo, remoteServerDetails, 0) } func getXrayLibPluginFullName() (string, error) { diff --git a/sca/bom/xrayplugin/xraylibbom.go b/sca/bom/xrayplugin/xraylibbom.go index 442d922e0..fae037adb 100644 --- a/sca/bom/xrayplugin/xraylibbom.go +++ b/sca/bom/xrayplugin/xraylibbom.go @@ -3,9 +3,9 @@ package xrayplugin import ( "fmt" "os/exec" - "time" "github.com/CycloneDX/cyclonedx-go" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-security/sca/bom" "github.com/jfrog/jfrog-cli-security/sca/bom/xrayplugin/plugin" "github.com/jfrog/jfrog-cli-security/utils" @@ -22,23 +22,17 @@ const SnippetDetectionFeatureId = "curation" type XrayLibBomGenerator struct { binaryPath string snippetDetection bool - ignorePatterns []string specificTechs []techutils.Technology - totalTargets int + + // Artifactory Repository params + ServerDetails *config.ServerDetails + DependenciesRepository string } func NewXrayLibBomGenerator() *XrayLibBomGenerator { return &XrayLibBomGenerator{} } -func WithTotalTargets(totalTargets int) bom.SbomGeneratorOption { - return func(sg bom.SbomGenerator) { - if sbg, ok := sg.(*XrayLibBomGenerator); ok { - sbg.totalTargets = totalTargets - } - } -} - func WithSpecificTechnologies(technologies []string) bom.SbomGeneratorOption { return func(sg bom.SbomGenerator) { if sbg, ok := sg.(*XrayLibBomGenerator); ok { @@ -50,18 +44,19 @@ func WithSpecificTechnologies(technologies []string) bom.SbomGeneratorOption { } } -func WithBinaryPath(binaryPath string) bom.SbomGeneratorOption { +func WithCentralRemoteReleasesDetails(serverDetails *config.ServerDetails, dependenciesRepository string) bom.SbomGeneratorOption { return func(sg bom.SbomGenerator) { if sbg, ok := sg.(*XrayLibBomGenerator); ok { - sbg.binaryPath = binaryPath + sbg.ServerDetails = serverDetails + sbg.DependenciesRepository = dependenciesRepository } } } -func WithIgnorePatterns(ignorePatterns []string) bom.SbomGeneratorOption { +func WithBinaryPath(binaryPath string) bom.SbomGeneratorOption { return func(sg bom.SbomGenerator) { if sbg, ok := sg.(*XrayLibBomGenerator); ok { - sbg.ignorePatterns = ignorePatterns + sbg.binaryPath = binaryPath } } } @@ -96,7 +91,7 @@ func (sbg *XrayLibBomGenerator) PrepareGenerator() (err error) { return } // Download the xray-lib plugin if needed - return plugin.DownloadXrayLibPluginIfNeeded() + return plugin.DownloadXrayLibPluginIfNeeded(sbg.DependenciesRepository, sbg.ServerDetails) } func (sbg *XrayLibBomGenerator) GenerateSbom(target results.ScanTarget) (sbom *cyclonedx.BOM, err error) { @@ -105,21 +100,15 @@ func (sbg *XrayLibBomGenerator) GenerateSbom(target results.ScanTarget) (sbom *c return nil, fmt.Errorf("failed to get local Xray-Lib executable path: %w", err) } log.Debug(fmt.Sprintf("Using Xray-Lib executable at: %s", binaryPath)) - startTime := time.Now() envVars := sbg.getPluginEnvVars() scanner, logPath, killPlugin, err := plugin.CreateScannerPluginClient(binaryPath, envVars) if err != nil { return nil, fmt.Errorf("failed to create Xray-Lib plugin client: %w", err) } defer killPlugin() - startLog := "Generating SBOM" - if sbg.totalTargets > 1 { - startLog += fmt.Sprintf(" for target: %s", target.Target) - } if logPath != "" { - startLog += fmt.Sprintf(" (plugin logs: %s)", logPath) + log.Debug(fmt.Sprintf("Plugin logs: %s", logPath)) } - log.Info(startLog + "...") if len(envVars) > 0 { log.Debug(fmt.Sprintf("Environment variables: %v", envVars)) } @@ -127,7 +116,6 @@ func (sbg *XrayLibBomGenerator) GenerateSbom(target results.ScanTarget) (sbom *c if sbom, err = sbg.executeScanner(scanner, target); err != nil { return nil, fmt.Errorf("failed to execute Xray-Lib command: %w", err) } - sbg.logScannerOutput(sbom, target.Target, startTime) return } @@ -145,7 +133,8 @@ func (sbg *XrayLibBomGenerator) executeScanner(scanner plugin.Scanner, target re BomRef: cdxutils.GetFileRef(target.Target), Type: string(cyclonedx.ComponentTypeFile), Name: target.Target, - IgnorePatterns: sbg.ignorePatterns, + IgnorePatterns: target.GetCentralConfigExclusions(utils.ScaScan), + IncludeDirs: target.Include, Ecosystems: sbg.specificTechs, } if scanConfigStr, err := utils.GetAsJsonString(scanConfig, false, true); err == nil { @@ -162,28 +151,6 @@ func (sbg *XrayLibBomGenerator) getPluginEnvVars() map[string]string { return envVars } -func (sbg *XrayLibBomGenerator) logScannerOutput(output *cyclonedx.BOM, target string, startTime time.Time) { - libComponents := []string{} - if output != nil && output.Components != nil { - for _, component := range *output.Components { - if component.Type == cyclonedx.ComponentTypeLibrary { - libComponents = append(libComponents, component.PackageURL) - } - } - } - outLog := "SBOM generated" - if sbg.totalTargets > 1 { - outLog += fmt.Sprintf(" for target '%s'", target) - } - outLog += ";" - if len(libComponents) == 0 { - outLog += " no library components were found" - } else { - outLog += fmt.Sprintf(" found %d library components", len(libComponents)) - } - log.Info(fmt.Sprintf("%s (duration %s)", outLog, time.Since(startTime).String())) -} - func (sbg *XrayLibBomGenerator) CleanUp() (err error) { // No cleanup needed for XrayLibBomGenerator return nil diff --git a/sca/scan/scascan.go b/sca/scan/scascan.go index 4bf917961..403816e8c 100644 --- a/sca/scan/scascan.go +++ b/sca/scan/scascan.go @@ -103,23 +103,20 @@ func shouldRunScan(params ScaScanParams) (bool, error) { } // If the scan is not requested, skip it. if len(params.ScansToPerform) > 0 && !slices.Contains(params.ScansToPerform, utils.ScaScan) { - log.Debug(fmt.Sprintf("%sSkipping SCA for %s as requested by input...", logPrefix, params.ScanResults.Target)) + log.Debug(fmt.Sprintf("%sSkipping SCA for '%s' as requested by input...", logPrefix, params.ScanResults.String())) return false, nil } + if params.ScanResults == nil { + return false, errors.New("scan results are nil in SCA scan parameters") + } // If the scan is turned off in the config profile, skip it. - if params.ConfigProfile != nil { - if len(params.ConfigProfile.Modules) < 1 { - // Verify Modules are not nil and contain at least one modules - return false, fmt.Errorf("config profile %s has no modules. A config profile must contain at least one modules", params.ConfigProfile.ProfileName) - } - if !params.ConfigProfile.Modules[0].ScanConfig.ScaScannerConfig.EnableScaScan { - log.Debug(fmt.Sprintf("%sSkipping SCA as requested by '%s' config profile...", logPrefix, params.ConfigProfile.ProfileName)) + if centralConfiguredToRun := params.ScanResults.IsScanRequestedByCentralConfig(utils.ScaScan); centralConfiguredToRun != nil { + log.Debug(fmt.Sprintf("Using config profile '%s' to determine if SCA should be performed...", params.ConfigProfile.ProfileName)) + if !*centralConfiguredToRun { + log.Debug(fmt.Sprintf("%sSkipping SCA for '%s' as requested by '%s' config profile...", logPrefix, params.ScanResults.String(), params.ConfigProfile.ProfileName)) return false, nil } } - if params.ScanResults == nil { - return false, errors.New("scan results are nil for target") - } return hasDependenciesToScan(params.ScanResults, logPrefix), nil } diff --git a/scans_test.go b/scans_test.go index ed25b6c04..367dfec2e 100644 --- a/scans_test.go +++ b/scans_test.go @@ -63,6 +63,10 @@ func subScansToFlags(subScans []utils.SubScanType) (flags []string) { flags = append(flags, "--sca") case utils.SecretsScan: flags = append(flags, "--secrets") + case utils.IacScan: + flags = append(flags, "--iac") + case utils.SastScan: + flags = append(flags, "--sast") } } return flags diff --git a/tests/utils/test_utils.go b/tests/utils/test_utils.go index 674d1dbcc..a4baef83f 100644 --- a/tests/utils/test_utils.go +++ b/tests/utils/test_utils.go @@ -479,7 +479,7 @@ func PrepareAnalyzerManagerResource() (err error) { } return nil } - return jas.DownloadAnalyzerManagerIfNeeded(0) + return jas.DownloadAnalyzerManagerIfNeeded("", nil, 0) } func PrepareIndexerAppResource(details *config.ServerDetails) (err error) { @@ -492,5 +492,5 @@ func PrepareIndexerAppResource(details *config.ServerDetails) (err error) { } func PrepareXrayScanLibResource() (err error) { - return plugin.DownloadXrayLibPluginIfNeeded() + return plugin.DownloadXrayLibPluginIfNeeded("", nil) } diff --git a/tests/validations/test_validate_simple_json.go b/tests/validations/test_validate_simple_json.go index 5f2e60b1b..6612c66fd 100644 --- a/tests/validations/test_validate_simple_json.go +++ b/tests/validations/test_validate_simple_json.go @@ -158,7 +158,6 @@ func validateVulnerabilityOrViolationRow(t *testing.T, exactMatch bool, expected StringValidation{Expected: expected.Summary, Actual: actual.Summary, Msg: fmt.Sprintf("IssueId %s: Summary mismatch", expected.IssueId)}, StringValidation{Expected: expected.Severity, Actual: actual.Severity, Msg: fmt.Sprintf("IssueId %s: Severity mismatch", expected.IssueId)}, StringValidation{Expected: expected.Applicable, Actual: actual.Applicable, Msg: fmt.Sprintf("IssueId %s: Applicable mismatch", expected.IssueId)}, - // StringValidation{Expected: expected.Technology.String(), Actual: actual.Technology.String(), Msg: fmt.Sprintf("IssueId %s: Technology mismatch", expected.IssueId)}, ListValidation[string]{Expected: expected.References, Actual: actual.References, Msg: fmt.Sprintf("IssueId %s: References mismatch", expected.IssueId)}, StringValidation{Expected: expected.ImpactedDependencyType, Actual: actual.ImpactedDependencyType, Msg: fmt.Sprintf("IssueId %s: ImpactedDependencyType mismatch", expected.IssueId)}, diff --git a/utils/formats/cdxutils/cyclonedxutils.go b/utils/formats/cdxutils/cyclonedxutils.go index 7236773bc..5e9a06d15 100644 --- a/utils/formats/cdxutils/cyclonedxutils.go +++ b/utils/formats/cdxutils/cyclonedxutils.go @@ -304,12 +304,13 @@ func SearchComponentByCleanPurl(components *[]cyclonedx.Component, purl string) return } -func CreateFileOrDirComponent(filePathOrUri string) (component cyclonedx.Component) { +func CreateFileOrDirComponent(filePathOrUri string, relatedProperties ...cyclonedx.Property) (component cyclonedx.Component) { component = cyclonedx.Component{ BOMRef: GetFileRef(filePathOrUri), Type: cyclonedx.ComponentTypeFile, Name: convertToFileUrlIfNeeded(filePathOrUri), } + component.Properties = AppendProperties(component.Properties, relatedProperties...) return } @@ -458,12 +459,21 @@ func GetTrimmedPurlByRef(dep string, components *[]cyclonedx.Component) string { return techutils.PurlToXrayComponentId(component.PackageURL) } +// ScanLicenseToCycloneDx maps an Xray license key/name to a CycloneDX license. +// CycloneDX allows only one of ID or Name, not both. +func ScanLicenseToCycloneDx(key, name string) *cyclonedx.License { + if key != "" { + return &cyclonedx.License{ID: key} + } + return &cyclonedx.License{Name: name} +} + func AttachLicenseToComponent(component *cyclonedx.Component, license cyclonedx.LicenseChoice) { if component.Licenses == nil { component.Licenses = &cyclonedx.Licenses{} } // Check if the license already exists in the component - if hasLicense(*component, license.License.ID) { + if hasLicense(*component, licenseIdentifier(license.License)) { // The license already exists, no need to add it again return } @@ -471,12 +481,22 @@ func AttachLicenseToComponent(component *cyclonedx.Component, license cyclonedx. *component.Licenses = append(*component.Licenses, license) } -func hasLicense(component cyclonedx.Component, licenseName string) bool { - if component.Licenses == nil || len(*component.Licenses) == 0 { +func licenseIdentifier(license *cyclonedx.License) string { + if license == nil { + return "" + } + if license.ID != "" { + return license.ID + } + return license.Name +} + +func hasLicense(component cyclonedx.Component, licenseId string) bool { + if licenseId == "" || component.Licenses == nil || len(*component.Licenses) == 0 { return false } for _, license := range *component.Licenses { - if license.License != nil && license.License.ID == licenseName { + if license.License != nil && licenseIdentifier(license.License) == licenseId { return true } } diff --git a/utils/formats/cdxutils/cyclonedxutils_test.go b/utils/formats/cdxutils/cyclonedxutils_test.go index d841e7591..f3851d895 100644 --- a/utils/formats/cdxutils/cyclonedxutils_test.go +++ b/utils/formats/cdxutils/cyclonedxutils_test.go @@ -1157,6 +1157,23 @@ func TestGetRootDependenciesEntries(t *testing.T) { } } +func TestScanLicenseToCycloneDx(t *testing.T) { + tests := []struct { + name string + key string + license string + expected cyclonedx.License + }{ + {name: "prefer key as id", key: "Apache-2.0-Key", license: "Apache-2.0", expected: cyclonedx.License{ID: "Apache-2.0-Key"}}, + {name: "fallback to name", key: "", license: "Apache-2.0", expected: cyclonedx.License{Name: "Apache-2.0"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, *ScanLicenseToCycloneDx(tt.key, tt.license)) + }) + } +} + func TestAttachLicenseToComponent(t *testing.T) { tests := []struct { name string diff --git a/utils/formats/sarifutils/sarifutils.go b/utils/formats/sarifutils/sarifutils.go index 35badac87..daa8d0489 100644 --- a/utils/formats/sarifutils/sarifutils.go +++ b/utils/formats/sarifutils/sarifutils.go @@ -894,6 +894,18 @@ func GetInvocationWorkingDirectory(invocation *sarif.Invocation) string { return "" } +func CreateNewInvocation(success bool, target string, includeDirs ...string) *sarif.Invocation { + wd := sarif.NewArtifactLocation().WithURI(utils.ToURI(target)) + if len(includeDirs) > 0 { + // Add properties to the working directory + properties := sarif.NewPropertyBag() + properties.Add("include", strings.Join(includeDirs, ",")) + wd.Properties = properties + } + invocation := sarif.NewInvocation().WithExecutionSuccessful(success).WithWorkingDirectory(wd) + return invocation +} + func GetRulesPropertyCount(property, value string, runs ...*sarif.Run) (count int) { for _, run := range runs { for _, rule := range run.Tool.Driver.Rules { diff --git a/utils/paths.go b/utils/paths.go index a83c3342b..40d866f9f 100644 --- a/utils/paths.go +++ b/utils/paths.go @@ -1,19 +1,22 @@ package utils import ( + "errors" "fmt" "net/url" "os" "path" "path/filepath" + "regexp" "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/utils/dependencies" + "github.com/jfrog/jfrog-client-go/artifactory/services/fspatterns" + clientUtils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" - - "github.com/jfrog/jfrog-cli-security/utils/techutils" ) const ( @@ -54,7 +57,7 @@ func GetCurationCacheFolder() (string, error) { return filepath.Join(curationFolder, "cache"), nil } -func GetCurationCacheFolderByTech(tech techutils.Technology) (projectDir string, err error) { +func GetCurationCacheFolderByTech(tech string) (projectDir string, err error) { pathHash, errFromHash := getProjectPathHash() if errFromHash != nil { err = errFromHash @@ -64,7 +67,7 @@ func GetCurationCacheFolderByTech(tech techutils.Technology) (projectDir string, if err != nil { return "", err } - projectDir = filepath.Join(curationFolder, tech.String(), pathHash) + projectDir = filepath.Join(curationFolder, tech, pathHash) return } @@ -92,6 +95,97 @@ func GetCurationNugetCacheFolder() (string, error) { return filepath.Join(curationFolder, "nuget"), nil } +func GetFullPathsWorkingDirs(workingDirs []string) ([]string, error) { + var fullPathsWorkingDirs []string + for _, wd := range workingDirs { + fullPathWd, err := filepath.Abs(wd) + if err != nil { + return nil, err + } + fullPathsWorkingDirs = append(fullPathsWorkingDirs, fullPathWd) + } + return fullPathsWorkingDirs, nil +} + +func IsPathExcluded(path string, exclusions []string) bool { + if len(exclusions) == 0 { + return false + } + match, err := regexp.MatchString(fspatterns.PrepareExcludePathPattern(exclusions, clientUtils.WildCardPattern, true), path) + if err != nil { + log.Warn("Failed to check if path is excluded:", err.Error()) + return false + } + return match +} + +func GetExcludePattern(excludePatterns []string, defaultExcludePatterns []string, isRecursive bool) string { + exclusions := excludePatterns + if len(exclusions) == 0 { + exclusions = defaultExcludePatterns + } + return fspatterns.PrepareExcludePathPattern(exclusions, clientUtils.WildCardPattern, isRecursive) +} + +func IsPathMatchesPatterns(rootPath, path string, isRecursive, fromRelativePath bool, patterns ...string) bool { + if len(patterns) == 0 { + return false + } + if fromRelativePath { + relativePath, err := filepath.Rel(rootPath, path) + if err != nil { + log.Warn("Failed to get relative path:", err.Error()) + return false + } + path = relativePath + } + match, err := regexp.MatchString(fspatterns.PrepareExcludePathPattern(patterns, clientUtils.WildCardPattern, isRecursive), path) + if err != nil { + log.Warn("Failed to check if path matches pattern:", err.Error()) + return false + } + return match +} + +func ListFilesAndDirs(rootPath string, isRecursive, excludeWithRelativePath, preserveSymlink bool, excludePathPattern string) (files, dirs []string, err error) { + filesOrDirsInPath, err := fspatterns.ListFiles(rootPath, isRecursive, true, excludeWithRelativePath, preserveSymlink, excludePathPattern) + if err != nil { + return + } + for _, path := range filesOrDirsInPath { + if isDir, e := fileutils.IsDirExists(path, preserveSymlink); e != nil { + err = errors.Join(err, fmt.Errorf("failed to check if %s is a directory: %w", path, e)) + continue + } else if isDir { + dirs = append(dirs, path) + } else { + files = append(files, path) + } + } + return +} + +func ListDirs(rootPath string, isRecursive, patternsFromRelativePath, preserveSymlink bool, excludePattern string, includePatterns ...string) (dirs []string, err error) { + filesOrDirsInPath, err := fspatterns.ListFiles(rootPath, isRecursive, true, patternsFromRelativePath, preserveSymlink, excludePattern) + if err != nil { + return + } + for _, path := range filesOrDirsInPath { + if isDir, e := fileutils.IsDirExists(path, preserveSymlink); e != nil { + err = errors.Join(err, fmt.Errorf("failed to check if %s is a directory: %w", path, e)) + continue + } else if isDir { + // Validate if the directory matches any of the include patterns + if len(includePatterns) > 0 && !IsPathMatchesPatterns(rootPath, path, isRecursive, patternsFromRelativePath, includePatterns...) { + log.Verbose(fmt.Sprintf("Skipping directory %s as it does not match any of the include patterns: %s", path, strings.Join(includePatterns, ", "))) + continue + } + dirs = append(dirs, path) + } + } + return +} + func GetRelativePath(fullPathWd, baseWd string) string { // Remove OS-specific file prefix if strings.HasPrefix(fullPathWd, "file:///private") { @@ -172,8 +266,14 @@ func ToURI(path string) string { return u.String() } -func GetReleasesRemoteDetails(artifact, downloadPath string) (server *config.ServerDetails, fullRemotePath string, err error) { - var remoteRepo string +func GetReleasesRemoteDetails(artifact, downloadPath, remoteRepo string, remoteServerDetails *config.ServerDetails) (server *config.ServerDetails, fullRemotePath string, err error) { + if remoteServerDetails != nil && remoteRepo != "" { + // Config profile server and repo details are provided + server = remoteServerDetails + fullRemotePath = path.Join(remoteRepo, downloadPath) + return + } + // Try to get releases remote details from environment variable server, remoteRepo, err = dependencies.GetRemoteDetails(coreutils.ReleasesRemoteEnv) if err != nil { return @@ -182,7 +282,7 @@ func GetReleasesRemoteDetails(artifact, downloadPath string) (server *config.Ser fullRemotePath = path.Join(remoteRepo, "artifactory", downloadPath) return } - log.Debug(fmt.Sprintf("'"+coreutils.ReleasesRemoteEnv+"' environment variable is not configured. The %s will be downloaded directly from releases.jfrog.io if needed.", artifact)) + log.Debug(fmt.Sprintf("'"+coreutils.ReleasesRemoteEnv+"' environment variable is not configured. The '%s' will be downloaded directly from releases.jfrog.io if needed.", artifact)) // If not configured to download through a remote repository in Artifactory, download from releases.jfrog.io. return &config.ServerDetails{ArtifactoryUrl: coreutils.JfrogReleasesUrl}, downloadPath, nil } diff --git a/utils/paths_test.go b/utils/paths_test.go new file mode 100644 index 000000000..dc670de8f --- /dev/null +++ b/utils/paths_test.go @@ -0,0 +1,85 @@ +package utils + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsPathExcluded(t *testing.T) { + tests := []struct { + name string + path string + exclusions []string + expected bool + }{ + { + name: "Matching exclusion pattern", + path: "/project/node_modules/pkg", + exclusions: []string{"*node_modules*"}, + expected: true, + }, + { + name: "Non-matching exclusion pattern", + path: "/project/src/main.go", + exclusions: []string{"*node_modules*"}, + expected: false, + }, + { + name: "Empty exclusions - path is not excluded", + path: "/project/src/main.go", + exclusions: []string{}, + expected: false, + }, + { + name: "Multiple patterns - one matches", + path: "/project/test/unit_test.go", + exclusions: []string{"*node_modules*", "*test*"}, + expected: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, IsPathExcluded(tt.path, tt.exclusions)) + }) + } +} + +func TestGetFullPathsWorkingDirs(t *testing.T) { + tests := []struct { + name string + workingDirs []string + expectErr bool + }{ + { + name: "Empty input", + workingDirs: []string{}, + expectErr: false, + }, + { + name: "Already absolute paths", + workingDirs: []string{"/absolute/path/one", "/absolute/path/two"}, + expectErr: false, + }, + { + name: "Relative paths get resolved", + workingDirs: []string{"relative/path"}, + expectErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := GetFullPathsWorkingDirs(tt.workingDirs) + if tt.expectErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Len(t, result, len(tt.workingDirs)) + for _, p := range result { + assert.True(t, filepath.IsAbs(p), "expected absolute path, got: %s", p) + } + }) + } +} diff --git a/utils/results/common.go b/utils/results/common.go index 67d9ce1e7..8003cf153 100644 --- a/utils/results/common.go +++ b/utils/results/common.go @@ -772,14 +772,10 @@ func CollectRuns(runs ...[]*sarif.Run) []*sarif.Run { return flat } -// Resolve the actual technology from multiple sources: -func GetIssueTechnology(responseTechnology string, targetTech techutils.Technology) techutils.Technology { - if responseTechnology != "" && responseTechnology != "generic" && (targetTech == "" || targetTech == "generic") { - // technology returned in the vulnerability/violation obj is the most specific technology - return techutils.ToTechnology(responseTechnology) - } - // if no technology is provided, use the target technology - return targetTech +// GetIssueTechnology resolves the most specific technology for an issue from the scan response, +// detected target technologies, and the impacted component package type. +func GetIssueTechnology(responseTechnology string, targetTechnologies []techutils.Technology, componentPackageType string) techutils.Technology { + return techutils.ResolveIssueTechnology(responseTechnology, targetTechnologies, componentPackageType) } // This function gets a list of xray scan responses that contain direct and indirect vulnerabilities and returns separate @@ -884,7 +880,7 @@ func GetTargetDirectDependencies(targetResult *TargetResults, flatTree, convertT // func extract -func SearchTargetResultsByRelativePath(relativeTarget string, technology techutils.Technology, resultsToCompare *SecurityCommandResults) (targetResults *TargetResults) { +func SearchTargetResultsByRelativePath(relativeTarget string, resultsToCompare *SecurityCommandResults, technologies ...techutils.Technology) (targetResults *TargetResults) { if resultsToCompare == nil || len(resultsToCompare.Targets) == 0 { log.Debug(fmt.Sprintf("No targets to compare in results for target '%s'", relativeTarget)) return @@ -892,7 +888,7 @@ func SearchTargetResultsByRelativePath(relativeTarget string, technology techuti // Results to compare could be a results from the same path or a relative path sourceBasePath := resultsToCompare.GetCommonParentPath() var best *TargetResults - log.Debug(fmt.Sprintf("Searching for target '%s' with technology '%s' in results with base path '%s'", relativeTarget, technology.String(), sourceBasePath)) + log.Debug(fmt.Sprintf("Searching for target '%s' with technology '%s' in results with base path '%s'", relativeTarget, techutils.ToFormalString(technologies), sourceBasePath)) defer func() { if best == nil { log.Debug("No target found") @@ -903,8 +899,9 @@ func SearchTargetResultsByRelativePath(relativeTarget string, technology techuti for _, potential := range resultsToCompare.Targets { relative := utils.GetRelativePath(potential.Target, sourceBasePath) log.Debug(fmt.Sprintf("Comparing target %s, relative: '%s'", potential.String(), relative)) - if technology != techutils.NoTech && potential.Technology != technology { + if len(technologies) > 0 && !utils.ElementsEqual(potential.Technologies, technologies) { // If the technology is not the same, skip the comparison + // When project evolves and new technologies are added, not supporting the new technology on the first change. continue } if relativeTarget == potential.Target { @@ -993,8 +990,9 @@ func CreateScaComponentFromXrayCompId(xrayImpactedPackageId string, properties . return } -// If pretty is true, return the formal technology name, otherwise return the cdx component type -func FormalTechOrCdxCompType(cdxCompType string, pretty bool) string { +// If pretty is true, return the formal technology name, otherwise return the cdx component type. +// targetTechnologies is used to disambiguate broad types (pypi, gav, npm) when pretty is true. +func FormalTechOrCdxCompType(cdxCompType string, pretty bool, targetTechnologies ...techutils.Technology) string { if !pretty { return cdxCompType } @@ -1002,6 +1000,9 @@ func FormalTechOrCdxCompType(cdxCompType string, pretty bool) string { if tech != techutils.NoTech { return tech.ToFormal() } + if resolved := GetIssueTechnology("", targetTechnologies, cdxCompType); resolved != techutils.NoTech { + return resolved.ToFormal() + } return cdxCompType } @@ -1281,10 +1282,7 @@ func ParseScanGraphLicenseToSbom(destination *cyclonedx.BOM) ParseLicenseFunc { affectedComponent := GetOrCreateScaComponent(destination, impactedPackagesId) // Attach the license to the component cdxutils.AttachLicenseToComponent(affectedComponent, cyclonedx.LicenseChoice{ - License: &cyclonedx.License{ - ID: license.Key, - Name: license.Name, - }, + License: cdxutils.ScanLicenseToCycloneDx(license.Key, license.Name), }) return nil } diff --git a/utils/results/common_test.go b/utils/results/common_test.go index e06262e3d..2cb74c40c 100644 --- a/utils/results/common_test.go +++ b/utils/results/common_test.go @@ -1,7 +1,6 @@ package results import ( - "os" "path" "path/filepath" "sort" @@ -1027,7 +1026,7 @@ func TestSearchTargetResultsByRelativePath(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - foundTarget := SearchTargetResultsByRelativePath(tc.target, techutils.NoTech, tc.cmdResults) + foundTarget := SearchTargetResultsByRelativePath(tc.target, tc.cmdResults) assert.Equal(t, tc.expectedFound, foundTarget != nil) }) } @@ -1037,35 +1036,60 @@ func TestSearchTargetResultsByRelativePathTechnologyDisambiguatesSameDirectory(t sharedDir := filepath.Join("root", "app") cmdResults := NewCommandResults(utils.SourceCode) // Order intentionally Poetry before Npm (simulates nondeterministic map iteration). - cmdResults.NewScanResults(ScanTarget{Target: sharedDir, Technology: techutils.Poetry}) - cmdResults.NewScanResults(ScanTarget{Target: sharedDir, Technology: techutils.Npm}) + cmdResults.NewScanResults(ScanTarget{Target: sharedDir, Technologies: []techutils.Technology{techutils.Poetry}}) + cmdResults.NewScanResults(ScanTarget{Target: sharedDir, Technologies: []techutils.Technology{techutils.Npm}}) // Same absolute path for every target ⇒ common parent equals that path ⇒ relative key is "" (see utils.GetRelativePath). relativeKey := utils.GetRelativePath(sharedDir, cmdResults.GetCommonParentPath()) require.Equal(t, "", relativeKey) t.Run("picks npm when requested", func(t *testing.T) { - found := SearchTargetResultsByRelativePath(relativeKey, techutils.Npm, cmdResults) + found := SearchTargetResultsByRelativePath(relativeKey, cmdResults, techutils.Npm) require.NotNil(t, found) - assert.Equal(t, techutils.Npm, found.Technology) + assert.Equal(t, techutils.Npm, found.Technologies[0]) assert.Equal(t, sharedDir, found.Target) }) t.Run("picks poetry when requested", func(t *testing.T) { - found := SearchTargetResultsByRelativePath(relativeKey, techutils.Poetry, cmdResults) + found := SearchTargetResultsByRelativePath(relativeKey, cmdResults, techutils.Poetry) require.NotNil(t, found) - assert.Equal(t, techutils.Poetry, found.Technology) + assert.Equal(t, techutils.Poetry, found.Technologies[0]) }) t.Run("reversed slice order still picks npm", func(t *testing.T) { reversed := NewCommandResults(utils.SourceCode) - reversed.NewScanResults(ScanTarget{Target: sharedDir, Technology: techutils.Npm}) - reversed.NewScanResults(ScanTarget{Target: sharedDir, Technology: techutils.Poetry}) + reversed.NewScanResults(ScanTarget{Target: sharedDir, Technologies: []techutils.Technology{techutils.Npm}}) + reversed.NewScanResults(ScanTarget{Target: sharedDir, Technologies: []techutils.Technology{techutils.Poetry}}) rel := utils.GetRelativePath(sharedDir, reversed.GetCommonParentPath()) - found := SearchTargetResultsByRelativePath(rel, techutils.Npm, reversed) + found := SearchTargetResultsByRelativePath(rel, reversed, techutils.Npm) require.NotNil(t, found) - assert.Equal(t, techutils.Npm, found.Technology) + assert.Equal(t, techutils.Npm, found.Technologies[0]) }) } +func TestFormalTechOrCdxCompType_MultiTech(t *testing.T) { + assert.Equal(t, "Poetry", FormalTechOrCdxCompType("pypi", true, techutils.Poetry)) + assert.Equal(t, "Yarn", FormalTechOrCdxCompType("npm", true, techutils.Yarn)) + assert.Equal(t, "pypi", FormalTechOrCdxCompType("pypi", false, techutils.Poetry)) +} + +func TestGetIssueTechnology(t *testing.T) { + tests := []struct { + name string + response string + compType string + targets []techutils.Technology + expected techutils.Technology + }{ + {"empty response npm target", "", "npm", []techutils.Technology{techutils.Npm}, techutils.Npm}, + {"pip response poetry target", "pip", "pypi", []techutils.Technology{techutils.Poetry}, techutils.Poetry}, + {"npm response disambiguate", "npm", "npm", []techutils.Technology{techutils.Maven, techutils.Npm}, techutils.Npm}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, GetIssueTechnology(tt.response, tt.targets, tt.compType)) + }) + } +} + func TestDepTreeToSbom(t *testing.T) { tests := []struct { name string @@ -2677,8 +2701,7 @@ func TestScanResponseToSbom(t *testing.T) { Licenses: &cyclonedx.Licenses{ { License: &cyclonedx.License{ - ID: "Apache-2.0-Key", - Name: "Apache-2.0", + ID: "Apache-2.0-Key", }, }, }, @@ -2844,7 +2867,6 @@ func TestScanResponseToSbom(t *testing.T) { } } // Validate - assert.NoError(t, cyclonedx.NewBOMEncoder(os.Stdout, cyclonedx.BOMFileFormatJSON).SetPretty(true).Encode(destination)) if expected.Components == nil { assert.Nil(t, destination.Components) } else { diff --git a/utils/results/conversion/convertor.go b/utils/results/conversion/convertor.go index 04fbf2d88..859ca3aee 100644 --- a/utils/results/conversion/convertor.go +++ b/utils/results/conversion/convertor.go @@ -117,7 +117,12 @@ func parseCommandResults[T interface{}](params ResultConvertParams, parser Resul // Skip this target as it's not in the include list continue } - if err = parser.ParseNewTargetResults(targetScansResults.ScanTarget, targetScansResults.Errors...); err != nil { + allTargetErrors := targetScansResults.GetAllErrors() + targetErrors := []error{} + for _, targetError := range allTargetErrors { + targetErrors = append(targetErrors, targetError.ActualError) + } + if err = parser.ParseNewTargetResults(targetScansResults.ScanTarget, targetErrors...); err != nil { return } if err = parseScaResults(params, parser, cmdResults.CmdType, targetScansResults); err != nil { @@ -145,7 +150,7 @@ func parseScaResults[T interface{}](params ResultConvertParams, parser ResultsSt return } } - if targetScansResults.ScaResults == nil || !utils.IsScanRequested(cmdType, utils.ScaScan, params.RequestedScans...) { + if targetScansResults.ScaResults == nil || !utils.IsScanRequested(cmdType, utils.ScaScan, targetScansResults.IsScanRequestedByCentralConfig(utils.ScaScan), params.RequestedScans...) { // Nothing to parse, no SCA results return } diff --git a/utils/results/conversion/convertor_test.go b/utils/results/conversion/convertor_test.go index d08950c85..07b56c8e6 100644 --- a/utils/results/conversion/convertor_test.go +++ b/utils/results/conversion/convertor_test.go @@ -162,7 +162,7 @@ func getAuditTestResults(unique bool) (*results.SecurityCommandResults, validati // Create basic command results to be converted to different formats cmdResults := results.NewCommandResults(utils.SourceCode) cmdResults.SetEntitledForJas(true).SetXrayVersion("3.107.13").SetXscVersion("1.12.5").SetMultiScanId("7d5e4733-3f93-11ef-8147-e610d09d7daa") - npmTargetResults := cmdResults.NewScanResults(results.ScanTarget{Target: filepath.Join("Users", "user", "project-with-issues"), Technology: techutils.Npm}).SetDescriptors(filepath.Join("Users", "user", "project-with-issues", "package.json")) + npmTargetResults := cmdResults.NewScanResults(results.ScanTarget{Target: filepath.Join("Users", "user", "project-with-issues"), Technologies: []techutils.Technology{techutils.Npm}}).SetDescriptors(filepath.Join("Users", "user", "project-with-issues", "package.json")) // SCA scan results npmTargetResults.ScaScanResults(0, services.ScanResponse{ ScanId: "711851ce-68c4-4dfd-7afb-c29737ebcb96", @@ -606,7 +606,7 @@ func getDockerScanTestResults(unique bool) (*results.SecurityCommandResults, val // Create basic command results to be converted to different formats cmdResults := results.NewCommandResults(utils.DockerImage) cmdResults.SetEntitledForJas(true).SetXrayVersion("3.107.13").SetXscVersion("1.12.5").SetMultiScanId("7d5e4733-3f93-11ef-8147-e610d09d7daa") - dockerImageTarget := cmdResults.NewScanResults(results.ScanTarget{Target: filepath.Join("temp", "folders", "T", "jfrog.cli.temp.-11-11", "image.tar"), Name: "platform.jfrog.io/swamp-docker/swamp:latest", Technology: techutils.Oci}) + dockerImageTarget := cmdResults.NewScanResults(results.ScanTarget{Target: filepath.Join("temp", "folders", "T", "jfrog.cli.temp.-11-11", "image.tar"), Name: "platform.jfrog.io/swamp-docker/swamp:latest", Technologies: []techutils.Technology{techutils.Oci}}) // SCA scan results dockerImageTarget.ScaScanResults(0, services.ScanResponse{ ScanId: "27da9106-88ea-416b-799b-bc7d15783473", diff --git a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go index 4d47c2771..f2b612173 100644 --- a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go +++ b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go @@ -3,6 +3,7 @@ package cyclonedxparser import ( "fmt" "os" + "strings" "time" "github.com/CycloneDX/cyclonedx-go" @@ -30,6 +31,8 @@ const ( secretValidationMetadataPropertyTemplate = "jfrog:secret-validation:metadata:" + results.LocationIdTemplate // Git context property gitContextProperty = "jfrog:git:context" + // Include directories property + includeDirectoriesProperty = "jfrog:include:directories" ) type CmdResultsCycloneDxConverter struct { @@ -92,7 +95,14 @@ func (cdc *CmdResultsCycloneDxConverter) ParseNewTargetResults(target results.Sc return results.ErrResetConvertor } cdc.currentTarget = target - cdc.setTargetComponent(target.Target, cdxutils.CreateFileOrDirComponent(target.Target)) + properties := []cyclonedx.Property{} + if len(target.Include) > 0 { + properties = append(properties, cyclonedx.Property{ + Name: includeDirectoriesProperty, + Value: strings.Join(target.Include, ","), + }) + } + cdc.setTargetComponent(target.Target, cdxutils.CreateFileOrDirComponent(target.Target, properties...)) return } diff --git a/utils/results/conversion/sarifparser/sarifparser.go b/utils/results/conversion/sarifparser/sarifparser.go index 923a0e5a2..2932e3eda 100644 --- a/utils/results/conversion/sarifparser/sarifparser.go +++ b/utils/results/conversion/sarifparser/sarifparser.go @@ -179,10 +179,7 @@ func (sc *CmdResultsSarifConverter) createScaRun(target results.ScanTarget, erro // For binary, the target is a file and not a directory wd = filepath.Dir(wd) } - run.Invocations = append(run.Invocations, sarif.NewInvocation(). - WithWorkingDirectory(sarif.NewSimpleArtifactLocation(utils.ToURI(wd))). - WithExecutionSuccessful(errorCount == 0), - ) + run.Invocations = append(run.Invocations, sarifutils.CreateNewInvocation(errorCount == 0, wd, target.Include...)) return run } diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser.go b/utils/results/conversion/simplejsonparser/simplejsonparser.go index 84280b41c..2609cb561 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser.go @@ -1,6 +1,7 @@ package simplejsonparser import ( + "fmt" "sort" "strings" @@ -14,6 +15,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/severityutils" "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray/services" "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" ) @@ -60,8 +62,14 @@ func (sjc *CmdResultsSimpleJsonConverter) Reset(metadata results.ResultsMetaData } sjc.entitledForJas = metadata.Entitlements.Jas sjc.multipleRoots = multipleTargets - if metadata.GeneralError != nil { - sjc.current.Errors = append(sjc.current.Errors, formats.SimpleJsonError{ErrorMessage: metadata.GeneralError.Error()}) + if scanErrors := metadata.GetAllErrors(); len(scanErrors) > 0 { + for _, scanError := range scanErrors { + if scanError.Skip { + log.Debug(fmt.Sprintf("Skipping adding error %s because it is skipped", scanError.ActualError.Error())) + continue + } + sjc.current.Errors = append(sjc.current.Errors, formats.SimpleJsonError{ErrorMessage: scanError.ActualError.Error()}) + } } return } @@ -115,7 +123,7 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseSbomLicenses(sbom *cyclonedx.BOM) ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ ImpactedDependencyName: normalizeCdxComponentName(compName, compType), ImpactedDependencyVersion: compVersion, - ImpactedDependencyType: results.FormalTechOrCdxCompType(compType, sjc.pretty), + ImpactedDependencyType: results.FormalTechOrCdxCompType(compType, sjc.pretty, sjc.currentTarget.Technologies...), Components: results.ExtractComponentDirectComponentsInBOM(bomIndex, component, impactPaths), }, ImpactPaths: impactPaths, @@ -236,13 +244,13 @@ func (sjc *CmdResultsSimpleJsonConverter) createVulnerabilityOrViolationRowFromC SeverityDetails: severityutils.GetAsDetails(severity, applicabilityStatus, sjc.pretty), ImpactedDependencyName: normalizeCdxComponentName(compName, compType), ImpactedDependencyVersion: compVersion, - ImpactedDependencyType: results.FormalTechOrCdxCompType(compType, sjc.pretty), + ImpactedDependencyType: results.FormalTechOrCdxCompType(compType, sjc.pretty, sjc.currentTarget.Technologies...), Components: directComponents, }, ImpactPaths: impactPaths, Cves: results.CdxVulnToCveRows(vulnerability, contextualAnalysis), FixedVersions: results.CdxToFixedVersions(fixedVersions), - Technology: results.GetIssueTechnology(compType, sjc.currentTarget.Technology), + Technology: results.GetIssueTechnology("", sjc.currentTarget.Technologies, compType), References: toReferences(vulnerability), Applicable: applicabilityStatus.ToString(sjc.pretty), JfrogResearchInformation: jfrogResearch, @@ -274,7 +282,7 @@ func (sjc *CmdResultsSimpleJsonConverter) createLicenseViolationRow(licenseKey, SeverityDetails: severityutils.GetAsDetails(severity, jasutils.NotScanned, sjc.pretty), ImpactedDependencyName: normalizeCdxComponentName(compName, compType), ImpactedDependencyVersion: compVersion, - ImpactedDependencyType: results.FormalTechOrCdxCompType(compType, sjc.pretty), + ImpactedDependencyType: results.FormalTechOrCdxCompType(compType, sjc.pretty, sjc.currentTarget.Technologies...), Components: directComponents, }, ImpactPaths: impactPaths, @@ -290,7 +298,7 @@ func (sjc *CmdResultsSimpleJsonConverter) createOpRiskViolationRow(opRiskViolati SeverityDetails: severityutils.GetAsDetails(opRiskViolation.Severity, jasutils.NotScanned, sjc.pretty), ImpactedDependencyName: normalizeCdxComponentName(compName, compType), ImpactedDependencyVersion: compVersion, - ImpactedDependencyType: results.FormalTechOrCdxCompType(compType, sjc.pretty), + ImpactedDependencyType: results.FormalTechOrCdxCompType(compType, sjc.pretty, sjc.currentTarget.Technologies...), Components: opRiskViolation.DirectComponents, }, RiskReason: opRiskViolation.RiskReason, @@ -416,7 +424,7 @@ func addSimpleJsonVulnerability(target results.ScanTarget, vulnerabilitiesRows * SeverityDetails: severityutils.GetAsDetails(severity, applicabilityStatus, pretty), ImpactedDependencyName: impactedPackagesName, ImpactedDependencyVersion: impactedPackagesVersion, - ImpactedDependencyType: results.FormalTechOrCdxCompType(impactedPackagesType, pretty), + ImpactedDependencyType: results.FormalTechOrCdxCompType(impactedPackagesType, pretty, target.Technologies...), Components: directComponents, }, FixedVersions: fixedVersion, @@ -425,7 +433,7 @@ func addSimpleJsonVulnerability(target results.ScanTarget, vulnerabilitiesRows * References: vulnerability.References, JfrogResearchInformation: results.ConvertJfrogResearchInformation(vulnerability.ExtendedInformation), ImpactPaths: impactPaths, - Technology: results.GetIssueTechnology(vulnerability.Technology, target.Technology), + Technology: results.GetIssueTechnology(vulnerability.Technology, target.Technologies, impactedPackagesType), Applicable: applicabilityStatus.ToString(pretty), }, ) @@ -435,11 +443,11 @@ func addSimpleJsonVulnerability(target results.ScanTarget, vulnerabilitiesRows * func PrepareSimpleJsonLicenses(target results.ScanTarget, licenses []services.License, pretty bool) ([]formats.LicenseRow, error) { var licensesRows []formats.LicenseRow - err := results.ForEachLicense(target, licenses, addSimpleJsonLicense(&licensesRows, pretty)) + err := results.ForEachLicense(target, licenses, addSimpleJsonLicense(target, &licensesRows, pretty)) return licensesRows, err } -func addSimpleJsonLicense(licenseViolationsRows *[]formats.LicenseRow, pretty bool) results.ParseLicenseFunc { +func addSimpleJsonLicense(target results.ScanTarget, licenseViolationsRows *[]formats.LicenseRow, pretty bool) results.ParseLicenseFunc { return func(license services.License, impactedPackagesId string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error { impactedPackagesName, impactedPackagesVersion, impactedPackagesType := techutils.SplitComponentId(impactedPackagesId) *licenseViolationsRows = append(*licenseViolationsRows, @@ -449,7 +457,7 @@ func addSimpleJsonLicense(licenseViolationsRows *[]formats.LicenseRow, pretty bo ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ ImpactedDependencyName: impactedPackagesName, ImpactedDependencyVersion: impactedPackagesVersion, - ImpactedDependencyType: results.FormalTechOrCdxCompType(impactedPackagesType, pretty), + ImpactedDependencyType: results.FormalTechOrCdxCompType(impactedPackagesType, pretty, target.Technologies...), Components: directComponents, }, }, diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser_test.go b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go index 9a84fd14b..72ef20781 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser_test.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go @@ -12,6 +12,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/jasutils" "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/severityutils" + "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-client-go/xray/services" ) @@ -487,3 +488,156 @@ func TestPrepareSimpleJsonJasIssues(t *testing.T) { }) } } + +func TestPrepareSimpleJsonVulnerabilities_Technology(t *testing.T) { + testCases := []struct { + name string + target results.ScanTarget + vulns []services.Vulnerability + expectedTechPerIssue map[string]techutils.Technology + }{ + { + name: "Target technology used as fallback", + target: results.ScanTarget{Target: "target", Technologies: []techutils.Technology{techutils.Npm}}, + vulns: []services.Vulnerability{ + { + IssueId: "XRAY-100", + Summary: "vuln without tech", + Severity: "High", + Components: map[string]services.Component{ + "npm://dep:1.0.0": { + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "npm://dep:1.0.0"}}}, + }, + }, + }, + }, + expectedTechPerIssue: map[string]techutils.Technology{ + "XRAY-100": techutils.Npm, + }, + }, + { + name: "Response technology takes precedence over empty target", + target: results.ScanTarget{Target: "target"}, + vulns: []services.Vulnerability{ + { + IssueId: "XRAY-200", + Summary: "vuln with response tech", + Severity: "Medium", + Technology: "pip", + Components: map[string]services.Component{ + "pip://dep:2.0.0": { + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "pip://dep:2.0.0"}}}, + }, + }, + }, + }, + expectedTechPerIssue: map[string]techutils.Technology{ + "XRAY-200": techutils.Pip, + }, + }, + { + name: "Target technology wins when response is generic", + target: results.ScanTarget{Target: "target", Technologies: []techutils.Technology{techutils.Maven}}, + vulns: []services.Vulnerability{ + { + IssueId: "XRAY-300", + Summary: "generic vuln", + Severity: "Low", + Technology: "generic", + Components: map[string]services.Component{ + "maven://dep:3.0.0": { + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "maven://dep:3.0.0"}}}, + }, + }, + }, + }, + expectedTechPerIssue: map[string]techutils.Technology{ + "XRAY-300": techutils.Maven, + }, + }, + { + name: "Target with multiple technologies and issues", + target: results.ScanTarget{Target: "target", Technologies: []techutils.Technology{techutils.Maven, techutils.Npm}}, + vulns: []services.Vulnerability{ + { + IssueId: "XRAY-400", + Summary: "vuln with npm tech", + Severity: "Medium", + Technology: "npm", + Components: map[string]services.Component{ + "npm://dep:4.0.0": { + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "npm://dep:4.0.0"}}}, + }, + }, + }, + { + IssueId: "XRAY-500", + Summary: "vuln with maven tech", + Severity: "Low", + Technology: "maven", + Components: map[string]services.Component{ + "maven://dep:5.0.0": { + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "maven://dep:5.0.0"}}}, + }, + }, + }, + }, + expectedTechPerIssue: map[string]techutils.Technology{ + "XRAY-400": techutils.Npm, + "XRAY-500": techutils.Maven, + }, + }, + { + name: "Poetry target with pypi response", + target: results.ScanTarget{Target: "target", Technologies: []techutils.Technology{techutils.Poetry}}, + vulns: []services.Vulnerability{ + { + IssueId: "XRAY-600", + Summary: "pypi vuln", + Severity: "High", + Technology: "pypi", + Components: map[string]services.Component{ + "pypi://dep:6.0.0": { + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "pypi://dep:6.0.0"}}}, + }, + }, + }, + }, + expectedTechPerIssue: map[string]techutils.Technology{ + "XRAY-600": techutils.Poetry, + }, + }, + { + name: "Gradle target with gav response", + target: results.ScanTarget{Target: "target", Technologies: []techutils.Technology{techutils.Gradle}}, + vulns: []services.Vulnerability{ + { + IssueId: "XRAY-800", + Summary: "gav vuln", + Severity: "Low", + Technology: "gav", + Components: map[string]services.Component{ + "gav://dep:8.0.0": { + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "gav://dep:8.0.0"}}}, + }, + }, + }, + }, + expectedTechPerIssue: map[string]techutils.Technology{ + "XRAY-800": techutils.Gradle, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rows, err := PrepareSimpleJsonVulnerabilities(tc.target, nil, services.ScanResponse{Vulnerabilities: tc.vulns}, false, false) + assert.NoError(t, err) + for _, row := range rows { + expectedTech, ok := tc.expectedTechPerIssue[row.IssueId] + if assert.True(t, ok, "unexpected issue %s in output", row.IssueId) { + assert.Equal(t, expectedTech, row.Technology, "IssueId %s: Technology mismatch", row.IssueId) + } + } + }) + } +} diff --git a/utils/results/conversion/tableparser/tableparser.go b/utils/results/conversion/tableparser/tableparser.go index 1eb3e3348..81bcfcccc 100644 --- a/utils/results/conversion/tableparser/tableparser.go +++ b/utils/results/conversion/tableparser/tableparser.go @@ -20,6 +20,7 @@ import ( type CmdResultsTableConverter struct { simpleJsonConvertor *simplejsonparser.CmdResultsSimpleJsonConverter sbomRows []formats.SbomTableRow + currentTarget results.ScanTarget // If supported, pretty print the output in the tables pretty bool } @@ -57,6 +58,7 @@ func (tc *CmdResultsTableConverter) Reset(metadata results.ResultsMetaData, stat } func (tc *CmdResultsTableConverter) ParseNewTargetResults(target results.ScanTarget, errors ...error) (err error) { + tc.currentTarget = target return tc.simpleJsonConvertor.ParseNewTargetResults(target, errors...) } @@ -125,7 +127,7 @@ func (tc *CmdResultsTableConverter) ParseSbom(sbom *cyclonedx.BOM) (err error) { tc.sbomRows = append(tc.sbomRows, formats.SbomTableRow{ Component: compName, Version: compVersion, - PackageType: results.FormalTechOrCdxCompType(compType, tc.pretty), + PackageType: results.FormalTechOrCdxCompType(compType, tc.pretty, tc.currentTarget.Technologies...), Relation: relationStr, // For sorting RelationPriority: relationPriority, diff --git a/utils/results/output/resultwriter.go b/utils/results/output/resultwriter.go index 8fc815032..90c9eb0b3 100644 --- a/utils/results/output/resultwriter.go +++ b/utils/results/output/resultwriter.go @@ -15,6 +15,7 @@ import ( "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" + "golang.org/x/exp/slices" ) type ResultsWriter struct { @@ -149,7 +150,7 @@ func (rw *ResultsWriter) createResultsConvertor(pretty bool) *conversion.Command PlatformUrl: rw.platformUrl, IsMultipleRoots: rw.isMultipleRoots, IncludeLicenses: rw.commandResults.IncludesLicenses(), - IncludeSbom: rw.commandResults.IncludeSbom(), + IncludeSbom: rw.commandResults.IncludesSbom(), IncludeVulnerabilities: rw.commandResults.IncludesVulnerabilities(), HasViolationContext: rw.showViolations || rw.commandResults.HasViolationContext(), RequestedScans: rw.subScansPerformed, @@ -274,7 +275,7 @@ func (rw *ResultsWriter) printTables() (err error) { } func (rw *ResultsWriter) printScaTablesIfNeeded(tableContent formats.ResultsTables) (err error) { - if utils.IsScanRequested(rw.commandResults.CmdType, utils.ScaScan, rw.subScansPerformed...) { + if utils.IsScanRequested(rw.commandResults.CmdType, utils.ScaScan, rw.commandResults.IsScanRequestedByCentralConfig(utils.ScaScan), rw.subScansPerformed...) { if rw.showViolations || rw.commandResults.HasViolationContext() { if err = PrintViolationsTable(tableContent, rw.commandResults.CmdType, rw.printExtended); err != nil { return @@ -291,14 +292,14 @@ func (rw *ResultsWriter) printScaTablesIfNeeded(tableContent formats.ResultsTabl } } } - if !rw.commandResults.IncludeSbom() { + if !rw.commandResults.ResultContext.IncludeSbom { return } return PrintSbomTable(tableContent, rw.commandResults.CmdType) } func (rw *ResultsWriter) printJasTablesIfNeeded(tableContent formats.ResultsTables, subScan utils.SubScanType, scanType jasutils.JasScanType) (err error) { - if !utils.IsScanRequested(rw.commandResults.CmdType, subScan, rw.subScansPerformed...) { + if !utils.IsScanRequested(rw.commandResults.CmdType, subScan, rw.commandResults.IsScanRequestedByCentralConfig(subScan), rw.subScansPerformed...) { return } if (rw.showViolations || rw.commandResults.HasViolationContext()) && len(rw.commandResults.ResultContext.GitRepoHttpsCloneUrl) > 0 { @@ -313,7 +314,7 @@ func (rw *ResultsWriter) printJasTablesIfNeeded(tableContent formats.ResultsTabl } func (rw *ResultsWriter) shouldPrintSecretValidationExtraMessage() bool { - return rw.commandResults.SecretValidation && utils.IsScanRequested(rw.commandResults.CmdType, utils.SecretsScan, rw.subScansPerformed...) + return rw.commandResults.IsSecretValidationActive(slices.Contains(rw.subScansPerformed, utils.SecretTokenValidationScan)) && utils.IsScanRequested(rw.commandResults.CmdType, utils.SecretsScan, rw.commandResults.IsScanRequestedByCentralConfig(utils.SecretsScan), rw.subScansPerformed...) } // PrintVulnerabilitiesTable prints the vulnerabilities in a table. diff --git a/utils/results/results.go b/utils/results/results.go index 8cb83ef0c..bc89c35fe 100644 --- a/utils/results/results.go +++ b/utils/results/results.go @@ -3,6 +3,7 @@ package results import ( "errors" "fmt" + "strings" "sync" "time" @@ -49,20 +50,39 @@ type SecurityCommandResults struct { type ResultsMetaData struct { // MultiScanId is a unique identifier that is used to group multiple scans together. - MultiScanId string `json:"multi_scan_id,omitempty"` - XrayVersion string `json:"xray_version"` - XscVersion string `json:"xsc_version,omitempty"` - Entitlements Entitlements `json:"entitlements"` - SecretValidation bool `json:"secret_validation"` - CmdType utils.CommandType `json:"command_type"` - ResultContext ResultContext `json:"result_context"` - GitContext *xscServices.XscGitInfoContext `json:"git_context,omitempty"` - StartTime time.Time `json:"start_time"` - ResultsPlatformUrl string `json:"results_platform_url,omitempty"` + MultiScanId string `json:"multi_scan_id,omitempty"` + XrayVersion string `json:"xray_version"` + XscVersion string `json:"xsc_version,omitempty"` + Entitlements Entitlements `json:"entitlements"` + SecretValidation bool `json:"secret_validation"` + CmdType utils.CommandType `json:"command_type"` + ResultContext ResultContext `json:"result_context"` + GitContext *xscServices.XscGitInfoContext `json:"git_context,omitempty"` + StartTime time.Time `json:"start_time"` + ResultsPlatformUrl string `json:"results_platform_url,omitempty"` + AllowPartialResults bool `json:"allow_partial_results,omitempty"` // GeneralError that occurred during the command execution - GeneralError error `json:"general_error,omitempty"` + GeneralErrors []SkippableError `json:"general_errors,omitempty"` } +func (rm *ResultsMetaData) GetAllErrors() (allErrors []SkippableError) { + return append(allErrors, rm.GeneralErrors...) +} + +func (rm *ResultsMetaData) GetNotSkippedErrors() (notSkippedErrors []error) { + for _, generalError := range rm.GetAllErrors() { + if generalError.Skip { + continue + } + notSkippedErrors = append(notSkippedErrors, generalError.ActualError) + } + return notSkippedErrors +} + +type SkippableError struct { + ActualError error `json:"error"` + Skip bool `json:"skip"` +} type Entitlements struct { Jas bool `json:"jas"` SnippetDetection bool `json:"snippet_detection"` @@ -183,14 +203,13 @@ func shouldUpdateStatus(currentStatus, newStatus *int) bool { type TargetResults struct { ScanTarget - AppsConfigModule *jfrogappsconfig.Module `json:"apps_config_module,omitempty"` // All scan results for the target ScaResults *ScaScanResults `json:"sca_scans,omitempty"` JasResults *JasScansResults `json:"jas_scans,omitempty"` ResultsStatus ResultsStatus `json:"status,omitempty"` // Errors that occurred during the scans - Errors []error `json:"errors,omitempty"` - errorsMutex sync.Mutex `json:"-"` + TargetErrors []SkippableError `json:"errors,omitempty"` + errorsMutex sync.Mutex `json:"-"` } type ScaScanResults struct { @@ -219,29 +238,168 @@ type JasScanResults struct { type ScanTarget struct { // Physical location of the target: Working directory (audit) / binary to scan (scan / docker scan) Target string `json:"target,omitempty"` + // Optional field to provide the include patterns of the target + Include []string `json:"include,omitempty"` + // Optional field to provide the exclude patterns of the target + Exclude []string `json:"exclude,omitempty"` // Logical name of the target (build name / module name / docker image name...) Name string `json:"name,omitempty"` - // Optional field (not used only in build scan) to provide the technology of the target - Technology techutils.Technology `json:"technology,omitempty"` + // Technologies detected or assigned for this target + Technologies []techutils.Technology `json:"technologies,omitempty"` + // Optional field to provide the deprecated apps config module for the target + DeprecatedAppsConfigModule *jfrogappsconfig.Module `json:"deprecated_apps_config_module,omitempty"` + // Optional field to provide the central config modules for the target + CentralConfigModules []xscServices.Module `json:"central_config_modules,omitempty"` +} + +func (st ScanTarget) DetectedTechnologies() []techutils.Technology { + seen := datastructures.MakeSet[techutils.Technology]() + technologies := make([]techutils.Technology, 0, len(st.Technologies)) + for _, tech := range st.Technologies { + if tech == techutils.NoTech || seen.Exists(tech) { + continue + } + seen.Add(tech) + technologies = append(technologies, tech) + } + return technologies } -func (st ScanTarget) Copy(newTarget string) ScanTarget { - return ScanTarget{Target: newTarget, Name: st.Name, Technology: st.Technology} +func (st ScanTarget) HasTechnology(tech techutils.Technology) bool { + for _, t := range st.Technologies { + if t == tech { + return true + } + } + return false } func (st ScanTarget) String() (str string) { - str = st.Target + if len(st.Include) > 0 { + relativePaths := []string{} + for _, path := range st.Include { + relativePaths = append(relativePaths, utils.GetRelativePath(path, st.Target)) + } + str = fmt.Sprintf("%s {%s}", st.Target, strings.Join(relativePaths, ", ")) + } else { + str = st.Target + } if st.Name != "" { + // If project name is provided, use it instead of the target path str = st.Name } - tech := st.Technology.String() - if tech == techutils.NoTech.String() { - tech = "unknown" + seenTechnologies := datastructures.MakeSet[string]() + formalTechnologies := make([]string, 0, len(st.Technologies)) + for _, tech := range st.Technologies { + if tech == techutils.NoTech { + continue + } + formal := tech.ToFormal() + if seenTechnologies.Exists(formal) { + continue + } + seenTechnologies.Add(formal) + formalTechnologies = append(formalTechnologies, formal) + } + if len(formalTechnologies) == 0 { + str += " [unknown]" + } else { + str += fmt.Sprintf(" [%s]", strings.Join(formalTechnologies, ", ")) } - str += fmt.Sprintf(" [%s]", tech) return } +func (st ScanTarget) IsScanRequestedByCentralConfig(scanType utils.SubScanType) *bool { + if len(st.CentralConfigModules) == 0 { + return nil + } + for _, module := range st.CentralConfigModules { + switch scanType { + case utils.ScaScan: + if module.ScanConfig.ScaScannerConfig.EnableScaScan { + return utils.NewBoolPtr(true) + } + case utils.ContextualAnalysisScan: + if module.ScanConfig.ContextualAnalysisScannerConfig.EnableCaScan && module.ScanConfig.ScaScannerConfig.EnableScaScan { + return utils.NewBoolPtr(true) + } + case utils.IacScan: + if module.ScanConfig.IacScannerConfig.EnableIacScan { + return utils.NewBoolPtr(true) + } + case utils.SecretsScan: + if module.ScanConfig.SecretsScannerConfig.EnableSecretsScan { + return utils.NewBoolPtr(true) + } + case utils.SastScan: + if module.ScanConfig.SastScannerConfig.EnableSastScan { + return utils.NewBoolPtr(true) + } + default: + return utils.NewBoolPtr(false) + } + } + return utils.NewBoolPtr(false) +} + +func (st ScanTarget) ShouldValidateSecrets(cliRequested bool) bool { + if len(st.CentralConfigModules) > 0 { + for _, module := range st.CentralConfigModules { + cfg := module.ScanConfig.SecretsScannerConfig + if cfg.EnableSecretsScan && cfg.ValidateSecrets { + return true + } + } + return false + } + return cliRequested +} + +func (st ScanTarget) GetCentralConfigExclusions(scanType utils.SubScanType) []string { + exclusions := datastructures.MakeSet[string]() + for _, module := range st.CentralConfigModules { + // Always add the general exclude patterns from the module + exclusions.AddElements(module.ExcludePatterns...) + // Add the exclude patterns for the specific scan type + switch scanType { + case utils.ScaScan: + exclusions.AddElements(module.ScanConfig.ScaScannerConfig.ExcludePatterns...) + case utils.ContextualAnalysisScan: + exclusions.AddElements(module.ScanConfig.ContextualAnalysisScannerConfig.ExcludePatterns...) + case utils.IacScan: + exclusions.AddElements(module.ScanConfig.IacScannerConfig.ExcludePatterns...) + case utils.SecretsScan: + exclusions.AddElements(module.ScanConfig.SecretsScannerConfig.ExcludePatterns...) + case utils.SastScan: + exclusions.AddElements(module.ScanConfig.SastScannerConfig.ExcludePatterns...) + } + } + return exclusions.ToSlice() +} + +func (st ScanTarget) GetDeprecatedAppsConfigModuleExclusions(scanType jasutils.JasScanType) []string { + if st.DeprecatedAppsConfigModule == nil { + return nil + } + exclusions := datastructures.MakeSet[string]() + exclusions.AddElements(st.DeprecatedAppsConfigModule.ExcludePatterns...) + switch scanType { + case jasutils.Secrets: + if st.DeprecatedAppsConfigModule.Scanners.Secrets != nil { + exclusions.AddElements(st.DeprecatedAppsConfigModule.Scanners.Secrets.ExcludePatterns...) + } + case jasutils.Sast: + if st.DeprecatedAppsConfigModule.Scanners.Sast != nil { + exclusions.AddElements(st.DeprecatedAppsConfigModule.Scanners.Sast.ExcludePatterns...) + } + case jasutils.IaC: + if st.DeprecatedAppsConfigModule.Scanners.Iac != nil { + exclusions.AddElements(st.DeprecatedAppsConfigModule.Scanners.Iac.ExcludePatterns...) + } + } + return exclusions.ToSlice() +} + func NewCommandResults(cmdType utils.CommandType) *SecurityCommandResults { return &SecurityCommandResults{ResultsMetaData: ResultsMetaData{CmdType: cmdType}, targetsMutex: sync.Mutex{}, errorsMutex: sync.Mutex{}} } @@ -276,6 +434,44 @@ func (r *SecurityCommandResults) SetSecretValidation(secretValidation bool) *Sec return r } +func (r *SecurityCommandResults) IsJASRequested(requestedScans ...utils.SubScanType) bool { + return utils.IsScanRequested(r.CmdType, utils.ContextualAnalysisScan, r.IsScanRequestedByCentralConfig(utils.ContextualAnalysisScan), requestedScans...) || + utils.IsScanRequested(r.CmdType, utils.SecretsScan, r.IsScanRequestedByCentralConfig(utils.SecretsScan), requestedScans...) || + utils.IsScanRequested(r.CmdType, utils.IacScan, r.IsScanRequestedByCentralConfig(utils.IacScan), requestedScans...) || + utils.IsScanRequested(r.CmdType, utils.SastScan, r.IsScanRequestedByCentralConfig(utils.SastScan), requestedScans...) +} + +func (r *SecurityCommandResults) IsScanRequestedByCentralConfig(scanType utils.SubScanType) (requested *bool) { + hadModules := false + for _, target := range r.Targets { + if len(target.CentralConfigModules) == 0 { + continue + } + hadModules = true + if targetRequested := target.IsScanRequestedByCentralConfig(scanType); targetRequested != nil && *targetRequested { + return targetRequested + } + } + if hadModules { + return utils.NewBoolPtr(false) + } + return nil +} + +// IsSecretValidationActive returns whether secret token validation is requested for any scan target. +// SecretValidation on the command results reflects system entitlement; cliRequested reflects the --validate-secrets flag. +func (r *SecurityCommandResults) IsSecretValidationActive(cliRequested bool) bool { + if !r.SecretValidation { + return false + } + for _, target := range r.Targets { + if target.ShouldValidateSecrets(cliRequested) { + return true + } + } + return false +} + func (r *SecurityCommandResults) SetMultiScanId(multiScanId string) *SecurityCommandResults { r.MultiScanId = multiScanId return r @@ -291,6 +487,11 @@ func (r *SecurityCommandResults) SetGitContext(gitContext *xscServices.XscGitInf return r } +func (r *SecurityCommandResults) SetAllowPartialResults(allowPartialResults bool) *SecurityCommandResults { + r.AllowPartialResults = allowPartialResults + return r +} + func (r *SecurityCommandResults) SetResultsPlatformUrl(resultsPlatformUrl string) *SecurityCommandResults { r.ResultsPlatformUrl = resultsPlatformUrl return r @@ -300,12 +501,14 @@ func (r *SecurityCommandResults) SetResultsPlatformUrl(resultsPlatformUrl string // Adds a general error to the command results in different phases of its execution. // Notice that in some usages we pass constant 'false' to the 'allowSkippingError' parameter in some places, where we wish to force propagation of the error when it occurs. func (r *SecurityCommandResults) AddGeneralError(err error, allowSkippingError bool) *SecurityCommandResults { - if allowSkippingError && err != nil { - log.Warn(fmt.Sprintf("Partial results are allowed, the error is skipped: %s", err.Error())) + if err == nil { return r } + if allowSkippingError { + log.Warn(fmt.Sprintf("Partial results are allowed, the error is skipped: %s", err.Error())) + } r.errorsMutex.Lock() - r.GeneralError = errors.Join(r.GeneralError, err) + r.GeneralErrors = append(r.GeneralErrors, SkippableError{ActualError: err, Skip: allowSkippingError}) r.errorsMutex.Unlock() return r } @@ -325,7 +528,8 @@ func (r *SecurityCommandResults) IncludesLicenses() bool { return r.ResultContext.IncludeLicenses } -func (r *SecurityCommandResults) IncludeSbom() bool { +// Is the result includes sbom +func (r *SecurityCommandResults) IncludesSbom() bool { return r.ResultContext.IncludeSbom } @@ -375,14 +579,26 @@ func (r *SecurityCommandResults) HasJasScansResults(scanType jasutils.JasScanTyp return false } +func (r *SecurityCommandResults) GetAllErrors() []SkippableError { + errors := r.ResultsMetaData.GetAllErrors() + for _, target := range r.Targets { + errors = append(errors, target.TargetErrors...) + } + return errors +} + func (r *SecurityCommandResults) GetErrors() (err error) { - err = r.GeneralError + for _, generalError := range r.GetNotSkippedErrors() { + err = errors.Join(err, generalError) + } for _, target := range r.Targets { - if targetErr := target.GetErrors(); targetErr != nil { - err = errors.Join(err, fmt.Errorf("target '%s' errors:\n%s", target.String(), targetErr)) + if targetErrs := target.GetNotSkippedErrors(); len(targetErrs) > 0 { + for _, targetErr := range targetErrs { + err = errors.Join(err, fmt.Errorf("target '%s' errors:\n%s", target.String(), targetErr.Error())) + } } } - return + return err } func (r *SecurityCommandResults) GetTechnologies(additionalTechs ...techutils.Technology) []techutils.Technology { @@ -461,8 +677,23 @@ func (r *SecurityCommandResults) NewScanResults(target ScanTarget) *TargetResult return targetResults } +func (sr *TargetResults) GetAllErrors() (errors []SkippableError) { + return append(errors, sr.TargetErrors...) +} + +func (sr *TargetResults) GetNotSkippedErrors() []error { + errors := []error{} + for _, targetErr := range sr.TargetErrors { + if targetErr.Skip { + continue + } + errors = append(errors, targetErr.ActualError) + } + return errors +} + func (sr *TargetResults) GetErrors() (err error) { - for _, targetErr := range sr.Errors { + for _, targetErr := range sr.GetNotSkippedErrors() { err = errors.Join(err, targetErr) } return @@ -510,9 +741,7 @@ func (sr *TargetResults) GetScaScansXrayResults() (results []services.ScanRespon func (sr *TargetResults) GetTechnologies() []techutils.Technology { technologiesSet := datastructures.MakeSet[techutils.Technology]() - if sr.Technology != "" { - technologiesSet.Add(sr.Technology) - } + technologiesSet.AddElements(sr.DetectedTechnologies()...) if sr.ScaResults == nil { return technologiesSet.ToSlice() } @@ -566,13 +795,16 @@ func (sr *TargetResults) HasFindings() bool { } func (sr *TargetResults) AddTargetError(err error, allowSkippingError bool) error { - if allowSkippingError && err != nil { - log.Warn(fmt.Sprintf("Partial results are allowed, the error is skipped in target '%s': %s", sr.String(), err.Error())) + if err == nil { return nil } sr.errorsMutex.Lock() - sr.Errors = append(sr.Errors, err) + sr.TargetErrors = append(sr.TargetErrors, SkippableError{ActualError: err, Skip: allowSkippingError}) sr.errorsMutex.Unlock() + if allowSkippingError { + log.Warn(fmt.Sprintf("Partial results are allowed, the error is skipped in target '%s': %s", sr.String(), err.Error())) + return nil + } return err } diff --git a/utils/results/results_test.go b/utils/results/results_test.go new file mode 100644 index 000000000..2bb5cec9b --- /dev/null +++ b/utils/results/results_test.go @@ -0,0 +1,376 @@ +package results + +import ( + "testing" + + jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/techutils" + xscServices "github.com/jfrog/jfrog-client-go/xsc/services" + "github.com/stretchr/testify/assert" +) + +func TestScanTarget_String(t *testing.T) { + tests := []struct { + name string + target ScanTarget + expected string + }{ + { + name: "Target only, no tech", + target: ScanTarget{Target: "/path/to/project"}, + expected: "/path/to/project [unknown]", + }, + { + name: "Target with technology", + target: ScanTarget{Target: "/path/to/project", Technologies: []techutils.Technology{techutils.Npm}}, + expected: "/path/to/project [npm]", + }, + { + name: "Target with name overrides path", + target: ScanTarget{Target: "/path/to/project", Name: "my-project", Technologies: []techutils.Technology{techutils.Go}}, + expected: "my-project [Go]", + }, + { + name: "Target with include dirs", + target: ScanTarget{ + Target: "/root", + Include: []string{"/root/sub1", "/root/sub2"}, + Technologies: []techutils.Technology{techutils.Maven}, + }, + expected: "/root {sub1, sub2} [Maven]", + }, + { + name: "Target with include dirs and name - name wins", + target: ScanTarget{ + Target: "/root", + Include: []string{"/root/sub1"}, + Name: "override-name", + Technologies: []techutils.Technology{techutils.Pip}, + }, + expected: "override-name [Pip]", + }, + { + name: "Target with multiple technologies", + target: ScanTarget{ + Target: "/path/to/project", + Technologies: []techutils.Technology{techutils.Npm, techutils.Go}, + }, + expected: "/path/to/project [npm, Go]", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.target.String()) + }) + } +} + +func TestScanTarget_IsScanRequestedByCentralConfig(t *testing.T) { + enabledModule := xscServices.Module{ + ScanConfig: xscServices.ScanConfig{ + ScaScannerConfig: xscServices.ScaScannerConfig{EnableScaScan: true}, + ContextualAnalysisScannerConfig: xscServices.CaScannerConfig{EnableCaScan: true}, + IacScannerConfig: xscServices.IacScannerConfig{EnableIacScan: true}, + SecretsScannerConfig: xscServices.SecretsScannerConfig{EnableSecretsScan: true}, + SastScannerConfig: xscServices.SastScannerConfig{EnableSastScan: true}, + }, + } + + tests := []struct { + name string + target ScanTarget + scanType utils.SubScanType + expected *bool + }{ + { + name: "No modules - returns nil", + target: ScanTarget{}, + scanType: utils.ScaScan, + expected: nil, + }, + { + name: "SCA enabled", + target: ScanTarget{CentralConfigModules: []xscServices.Module{enabledModule}}, + scanType: utils.ScaScan, + expected: utils.NewBoolPtr(true), + }, + { + name: "IaC enabled", + target: ScanTarget{CentralConfigModules: []xscServices.Module{enabledModule}}, + scanType: utils.IacScan, + expected: utils.NewBoolPtr(true), + }, + { + name: "Secrets enabled", + target: ScanTarget{CentralConfigModules: []xscServices.Module{enabledModule}}, + scanType: utils.SecretsScan, + expected: utils.NewBoolPtr(true), + }, + { + name: "SAST enabled", + target: ScanTarget{CentralConfigModules: []xscServices.Module{enabledModule}}, + scanType: utils.SastScan, + expected: utils.NewBoolPtr(true), + }, + { + name: "Applicability requires both CA and SCA enabled", + target: ScanTarget{CentralConfigModules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + ContextualAnalysisScannerConfig: xscServices.CaScannerConfig{EnableCaScan: true}, + ScaScannerConfig: xscServices.ScaScannerConfig{EnableScaScan: false}, + }, + }}}, + scanType: utils.ContextualAnalysisScan, + expected: utils.NewBoolPtr(false), + }, + { + name: "Applicability with both CA and SCA enabled", + target: ScanTarget{CentralConfigModules: []xscServices.Module{enabledModule}}, + scanType: utils.ContextualAnalysisScan, + expected: utils.NewBoolPtr(true), + }, + { + name: "SCA disabled", + target: ScanTarget{CentralConfigModules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + ScaScannerConfig: xscServices.ScaScannerConfig{EnableScaScan: false}, + }, + }}}, + scanType: utils.ScaScan, + expected: utils.NewBoolPtr(false), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.target.IsScanRequestedByCentralConfig(tt.scanType)) + }) + } +} + +func TestScanTarget_ShouldValidateSecrets(t *testing.T) { + moduleWithValidation := xscServices.Module{ + ScanConfig: xscServices.ScanConfig{ + SecretsScannerConfig: xscServices.SecretsScannerConfig{ + EnableSecretsScan: true, + ValidateSecrets: true, + }, + }, + } + moduleWithoutValidation := xscServices.Module{ + ScanConfig: xscServices.ScanConfig{ + SecretsScannerConfig: xscServices.SecretsScannerConfig{ + EnableSecretsScan: true, + ValidateSecrets: false, + }, + }, + } + moduleValidationDisabledSecrets := xscServices.Module{ + ScanConfig: xscServices.ScanConfig{ + SecretsScannerConfig: xscServices.SecretsScannerConfig{ + EnableSecretsScan: false, + ValidateSecrets: true, + }, + }, + } + + tests := []struct { + name string + target ScanTarget + cliRequested bool + expected bool + }{ + { + name: "No modules - CLI requested", + target: ScanTarget{}, + cliRequested: true, + expected: true, + }, + { + name: "No modules - CLI not requested", + target: ScanTarget{}, + cliRequested: false, + expected: false, + }, + { + name: "Module with validate_secrets enabled", + target: ScanTarget{CentralConfigModules: []xscServices.Module{moduleWithValidation}}, + cliRequested: false, + expected: true, + }, + { + name: "Module with validate_secrets disabled - CLI ignored", + target: ScanTarget{CentralConfigModules: []xscServices.Module{moduleWithoutValidation}}, + cliRequested: true, + expected: false, + }, + { + name: "ValidateSecrets without EnableSecretsScan - false", + target: ScanTarget{CentralConfigModules: []xscServices.Module{moduleValidationDisabledSecrets}}, + cliRequested: true, + expected: false, + }, + { + name: "Any module with validation enabled", + target: ScanTarget{CentralConfigModules: []xscServices.Module{ + moduleWithoutValidation, + moduleWithValidation, + }}, + cliRequested: false, + expected: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.target.ShouldValidateSecrets(tt.cliRequested)) + }) + } +} + +func TestSecurityCommandResults_IsSecretValidationActive(t *testing.T) { + moduleWithValidation := xscServices.Module{ + ScanConfig: xscServices.ScanConfig{ + SecretsScannerConfig: xscServices.SecretsScannerConfig{ + EnableSecretsScan: true, + ValidateSecrets: true, + }, + }, + } + cmdResults := NewCommandResults(utils.SourceCode).SetSecretValidation(true) + cmdResults.NewScanResults(ScanTarget{CentralConfigModules: []xscServices.Module{moduleWithValidation}}) + + assert.True(t, cmdResults.IsSecretValidationActive(false)) + assert.False(t, NewCommandResults(utils.SourceCode).SetSecretValidation(false).IsSecretValidationActive(true)) +} + +func TestScanTarget_GetCentralConfigExclusions(t *testing.T) { + tests := []struct { + name string + target ScanTarget + scanType utils.SubScanType + expected []string + }{ + { + name: "No modules - empty", + target: ScanTarget{}, + scanType: utils.ScaScan, + expected: []string{}, + }, + { + name: "SCA exclusions", + target: ScanTarget{CentralConfigModules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + ScaScannerConfig: xscServices.ScaScannerConfig{ExcludePatterns: []string{"**/vendor/**"}}, + }, + }}}, + scanType: utils.ScaScan, + expected: []string{"**/vendor/**"}, + }, + { + name: "Secrets exclusions", + target: ScanTarget{CentralConfigModules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + SecretsScannerConfig: xscServices.SecretsScannerConfig{ExcludePatterns: []string{"**/*.key"}}, + }, + }}}, + scanType: utils.SecretsScan, + expected: []string{"**/*.key"}, + }, + { + name: "IaC exclusions", + target: ScanTarget{CentralConfigModules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + IacScannerConfig: xscServices.IacScannerConfig{ExcludePatterns: []string{"**/test-infra/**"}}, + }, + }}}, + scanType: utils.IacScan, + expected: []string{"**/test-infra/**"}, + }, + { + name: "SAST exclusions", + target: ScanTarget{CentralConfigModules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + SastScannerConfig: xscServices.SastScannerConfig{ExcludePatterns: []string{"**/generated/**"}}, + }, + }}}, + scanType: utils.SastScan, + expected: []string{"**/generated/**"}, + }, + { + name: "Contextual analysis exclusions", + target: ScanTarget{CentralConfigModules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + ContextualAnalysisScannerConfig: xscServices.CaScannerConfig{ExcludePatterns: []string{"**/mock/**"}}, + }, + }}}, + scanType: utils.ContextualAnalysisScan, + expected: []string{"**/mock/**"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.ElementsMatch(t, tt.expected, tt.target.GetCentralConfigExclusions(tt.scanType)) + }) + } +} + +func TestScanTarget_GetDeprecatedAppsConfigModuleExclusions(t *testing.T) { + tests := []struct { + name string + target ScanTarget + scanType jasutils.JasScanType + expected []string + }{ + { + name: "Nil module - returns nil", + target: ScanTarget{}, + scanType: jasutils.Secrets, + expected: nil, + }, + { + name: "Module with base exclusions only", + target: ScanTarget{DeprecatedAppsConfigModule: &jfrogappsconfig.Module{ + ExcludePatterns: []string{"**/dist/**"}, + }}, + scanType: jasutils.IaC, + expected: []string{"**/dist/**"}, + }, + { + name: "Module with secrets scanner exclusions", + target: ScanTarget{DeprecatedAppsConfigModule: &jfrogappsconfig.Module{ + ExcludePatterns: []string{"**/dist/**"}, + Scanners: jfrogappsconfig.Scanners{ + Secrets: &jfrogappsconfig.Scanner{ExcludePatterns: []string{"**/*.pem"}}, + }, + }}, + scanType: jasutils.Secrets, + expected: []string{"**/dist/**", "**/*.pem"}, + }, + { + name: "Module with SAST scanner exclusions", + target: ScanTarget{DeprecatedAppsConfigModule: &jfrogappsconfig.Module{ + Scanners: jfrogappsconfig.Scanners{ + Sast: &jfrogappsconfig.SastScanner{Scanner: jfrogappsconfig.Scanner{ExcludePatterns: []string{"**/test/**"}}}, + }, + }}, + scanType: jasutils.Sast, + expected: []string{"**/test/**"}, + }, + { + name: "Module with IaC scanner exclusions", + target: ScanTarget{DeprecatedAppsConfigModule: &jfrogappsconfig.Module{ + Scanners: jfrogappsconfig.Scanners{ + Iac: &jfrogappsconfig.Scanner{ExcludePatterns: []string{"**/sandbox/**"}}, + }, + }}, + scanType: jasutils.IaC, + expected: []string{"**/sandbox/**"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.ElementsMatch(t, tt.expected, tt.target.GetDeprecatedAppsConfigModuleExclusions(tt.scanType)) + }) + } +} diff --git a/utils/scm/gitmanager.go b/utils/scm/gitmanager.go index ecd29ccde..05de709a8 100644 --- a/utils/scm/gitmanager.go +++ b/utils/scm/gitmanager.go @@ -134,7 +134,7 @@ func (gm *GitManager) GetSourceControlContext() (gitInfo *services.XscGitInfoCon if !isClean { return nil, fmt.Errorf("uncommitted changes found in the repository, not supported in git audit") } - log.Debug(fmt.Sprintf("Git Context: %+v", gitInfo)) + log.Verbose(fmt.Sprintf("Git Context: %+v", gitInfo)) return gitInfo, nil } diff --git a/utils/techutils/techutils.go b/utils/techutils/techutils.go index f49024673..da4d2d768 100644 --- a/utils/techutils/techutils.go +++ b/utils/techutils/techutils.go @@ -9,14 +9,13 @@ import ( "strconv" "strings" - "golang.org/x/exp/maps" "golang.org/x/text/cases" "golang.org/x/text/language" "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/common/project" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-client-go/artifactory/services/fspatterns" + "github.com/jfrog/jfrog-cli-security/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" @@ -342,6 +341,14 @@ func (tech Technology) ToFormal() string { return technologiesData[tech].formal } +func ToFormalString(technologies []Technology) string { + formals := make([]string, len(technologies)) + for i, tech := range technologies { + formals[i] = tech.ToFormal() + } + return strings.Join(formals, ", ") +} + func (tech Technology) String() string { return string(tech) } @@ -440,7 +447,7 @@ func detectedTechnologiesListInPath(path string, recursive bool) (technologies [ // If requestedTechs is empty, all technologies will be checked. // If excludePathPattern is not empty, files/directories that match the wildcard pattern will be excluded from the search. func DetectTechnologiesDescriptors(path string, recursive bool, requestedTechs []string, requestedDescriptors map[Technology][]string, excludePathPattern string) (technologiesDetected map[Technology]map[string][]string, err error) { - filesList, dirsList, err := listFilesAndDirs(path, recursive, true, true, excludePathPattern) + filesList, dirsList, err := utils.ListFilesAndDirs(path, recursive, true, true, excludePathPattern) if err != nil { return } @@ -463,29 +470,18 @@ func DetectTechnologiesDescriptors(path string, recursive bool, requestedTechs [ technologiesDetected = addNoTechIfNeeded(technologiesDetected, path, dirsList) } techCount := len(technologiesDetected) + detectedTechs := datastructures.MakeSet[Technology]() + for tech := range technologiesDetected { + if tech == NoTech { + continue + } + detectedTechs.Add(tech) + } if _, exist := technologiesDetected[NoTech]; exist { techCount-- } if techCount > 0 { - log.Debug(fmt.Sprintf("Detected %d technologies at %s: %s.", techCount, path, maps.Keys(technologiesDetected))) - } - return -} - -func listFilesAndDirs(rootPath string, isRecursive, excludeWithRelativePath, preserveSymlink bool, excludePathPattern string) (files, dirs []string, err error) { - filesOrDirsInPath, err := fspatterns.ListFiles(rootPath, isRecursive, true, excludeWithRelativePath, preserveSymlink, excludePathPattern) - if err != nil { - return - } - for _, path := range filesOrDirsInPath { - if isDir, e := fileutils.IsDirExists(path, preserveSymlink); e != nil { - err = errors.Join(err, fmt.Errorf("failed to check if %s is a directory: %w", path, e)) - continue - } else if isDir { - dirs = append(dirs, path) - } else { - files = append(files, path) - } + log.Debug(fmt.Sprintf("Detected %d technologies at %s: %s.", techCount, path, detectedTechs.ToSlice())) } return } @@ -888,6 +884,94 @@ func CdxPackageTypeToXrayPackageType(cdxPackageType string) string { return cdxPackageType } +// IsAmbiguousTechnologyString returns true for Xray/CDX types that map to multiple package managers (pypi, gav, npm, etc.). +func IsAmbiguousTechnologyString(technology string) bool { + switch strings.ToLower(technology) { + case "", "generic", Pypi, Gav, string(Npm): + return true + default: + return false + } +} + +// technologySharesXrayEcosystem reports whether tech belongs to the same Xray ecosystem as xrayType. +func technologySharesXrayEcosystem(tech Technology, xrayType string) bool { + if tech.GetXrayPackageType() == xrayType { + return true + } + switch xrayType { + case Pypi: + return tech == Pip || tech == Pipenv || tech == Poetry + case Gav: + return tech == Maven || tech == Gradle + case string(Npm): + return tech == Npm || tech == Pnpm || tech == Yarn + default: + return false + } +} + +// ComponentPackageTypeToXrayType normalizes a CDX PURL type or Xray component-id type to an Xray package type. +func ComponentPackageTypeToXrayType(packageType string) string { + if packageType == "" { + return "" + } + switch strings.ToLower(packageType) { + case Pypi, Gav: + return strings.ToLower(packageType) + default: + return CdxPackageTypeToXrayPackageType(packageType) + } +} + +// TechnologiesInTargetsWithXrayType returns target technologies that share the given Xray package type ecosystem. +func TechnologiesInTargetsWithXrayType(targets []Technology, xrayType string) []Technology { + if xrayType == "" { + return nil + } + var matches []Technology + for _, tech := range targets { + if tech == NoTech { + continue + } + if technologySharesXrayEcosystem(tech, xrayType) { + matches = append(matches, tech) + } + } + return matches +} + +func containsTechnology(targets []Technology, tech Technology) bool { + for _, t := range targets { + if t == tech { + return true + } + } + return false +} + +// ResolveIssueTechnology picks the most specific technology for an SCA issue using the Xray response, detected target +// technologies, and the impacted component package type. +func ResolveIssueTechnology(responseTechnology string, targetTechnologies []Technology, componentPackageType string) Technology { + responseTech := ToTechnology(responseTechnology) + xrayType := ComponentPackageTypeToXrayType(componentPackageType) + candidates := TechnologiesInTargetsWithXrayType(targetTechnologies, xrayType) + + if responseTech != NoTech && !IsAmbiguousTechnologyString(responseTechnology) && containsTechnology(candidates, responseTech) { + return responseTech + } + if len(candidates) == 1 { + return candidates[0] + } + if responseTech != NoTech && !IsAmbiguousTechnologyString(responseTechnology) { + return responseTech + } + if len(targetTechnologies) == 1 && targetTechnologies[0] != NoTech { + return targetTechnologies[0] + } + return NoTech +} + // https://github.com/package-url/purl-spec/blob/main/PURL-SPECIFICATION.rst // Parse a given Package URL (purl) and return the component namespace, name, version, and package type. func SplitPackageUrlWithQualifiers(purl string) (packageType, compNamespace, compName, compVersion string, qualifiers map[string]string) { diff --git a/utils/techutils/techutils_test.go b/utils/techutils/techutils_test.go index 87f9d3c0a..c95751f23 100644 --- a/utils/techutils/techutils_test.go +++ b/utils/techutils/techutils_test.go @@ -862,6 +862,32 @@ func TestSplitPackageURL(t *testing.T) { } } +func TestResolveIssueTechnology(t *testing.T) { + tests := []struct { + name string + response string + compType string + targets []Technology + expected Technology + }{ + {"empty response npm target", "", "npm", []Technology{Npm}, Npm}, + {"pip response poetry target", "pip", "pypi", []Technology{Poetry}, Poetry}, + {"pypi response poetry target", "pypi", "pypi", []Technology{Poetry}, Poetry}, + {"gav response gradle target", "gav", "gav", []Technology{Gradle}, Gradle}, + {"npm response disambiguate", "npm", "npm", []Technology{Maven, Npm}, Npm}, + {"maven response disambiguate", "maven", "maven", []Technology{Maven, Npm}, Maven}, + {"generic uses target", "generic", "maven", []Technology{Maven}, Maven}, + {"yarn npm type", "yarn", "npm", []Technology{Yarn}, Yarn}, + {"single target fallback", "", "go", []Technology{Go}, Go}, + {"no match", "", "pypi", []Technology{Maven, Npm}, NoTech}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, ResolveIssueTechnology(tt.response, tt.targets, tt.compType)) + }) + } +} + func TestCdxPackageTypeToTechnology(t *testing.T) { tests := []struct { name string diff --git a/utils/utils.go b/utils/utils.go index 467895696..4c9ea12b8 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -24,6 +24,7 @@ import ( "time" "github.com/jfrog/gofrog/datastructures" + "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/utils/dependencies" clientutils "github.com/jfrog/jfrog-client-go/utils" @@ -115,7 +116,10 @@ func GetAllSupportedScans() []SubScanType { } // IsScanRequested returns true if the scan is requested, otherwise false. If requestedScans is empty, all scans are considered requested. -func IsScanRequested(cmdType CommandType, subScan SubScanType, requestedScans ...SubScanType) bool { +func IsScanRequested(cmdType CommandType, subScan SubScanType, centralConfigRequestedParam *bool, requestedScans ...SubScanType) bool { + if centralConfigRequestedParam != nil { + return *centralConfigRequestedParam + } if cmdType.IsTargetBinary() && (subScan == IacScan || subScan == SastScan) { return false } @@ -126,13 +130,6 @@ func IsScanRequested(cmdType CommandType, subScan SubScanType, requestedScans .. return len(requestedScans) == 0 || slices.Contains(requestedScans, subScan) } -func IsJASRequested(cmdType CommandType, requestedScans ...SubScanType) bool { - return IsScanRequested(cmdType, ContextualAnalysisScan, requestedScans...) || - IsScanRequested(cmdType, SecretsScan, requestedScans...) || - IsScanRequested(cmdType, IacScan, requestedScans...) || - IsScanRequested(cmdType, SastScan, requestedScans...) -} - func getScanFindingName(scanType SubScanType) string { if scanType == SecretsScan { return fmt.Sprintf("%s exposures", subScanTypeToText[scanType]) @@ -390,9 +387,9 @@ func GetGitRepoUrlKey(gitRepoHttpsCloneUrl string) string { return xscutils.GetGitRepoUrlKey(gitRepoHttpsCloneUrl) } -func DownloadResourceFromPlatformIfNeeded(resourceName, downloadPath, targetDir, targetArtifactName string, explodeArtifact bool, threadId int) error { +func DownloadResourceFromPlatformIfNeeded(resourceName, downloadPath, targetDir, targetArtifactName string, explodeArtifact bool, remoteRepo string, remoteServerDetails *config.ServerDetails, threadId int) error { // Get JPD / Releases remote details to download the resource from. - rtDetails, remotePath, err := GetReleasesRemoteDetails(resourceName, downloadPath) + rtDetails, remotePath, err := GetReleasesRemoteDetails(resourceName, downloadPath, remoteRepo, remoteServerDetails) if err != nil { return err } @@ -492,3 +489,20 @@ func (p *LineDecoratorWriter) Write(data []byte) (n int, err error) { } } } + +func ElementsEqual[T comparable](slice1 []T, slice2 []T) bool { + if len(slice1) != len(slice2) { + return false + } + freq := make(map[T]int, len(slice1)) + for _, v := range slice1 { + freq[v]++ + } + for _, v := range slice2 { + freq[v]-- + if freq[v] < 0 { + return false + } + } + return true +} diff --git a/utils/utils_test.go b/utils/utils_test.go index 7629efe11..5aa8c1cef 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -163,3 +163,72 @@ func TestMergeMaps(t *testing.T) { }) } } + +func TestElementsEqual(t *testing.T) { + testCases := []struct { + name string + slice1 []string + slice2 []string + expected bool + }{ + { + name: "Both empty", + slice1: []string{}, + slice2: []string{}, + expected: true, + }, + { + name: "Equal single element", + slice1: []string{"a"}, + slice2: []string{"a"}, + expected: true, + }, + { + name: "Same elements different order", + slice1: []string{"a", "b", "c"}, + slice2: []string{"c", "a", "b"}, + expected: true, + }, + { + name: "Different lengths", + slice1: []string{"a", "b"}, + slice2: []string{"a"}, + expected: false, + }, + { + name: "Same length different elements", + slice1: []string{"a", "b"}, + slice2: []string{"a", "c"}, + expected: false, + }, + { + name: "Duplicate mismatch", + slice1: []string{"a", "a", "b"}, + slice2: []string{"a", "b", "b"}, + expected: false, + }, + { + name: "Equal with duplicates", + slice1: []string{"a", "a", "b"}, + slice2: []string{"a", "b", "a"}, + expected: true, + }, + { + name: "Different with duplicates", + slice1: []string{"a", "b", "c"}, + slice2: []string{"a", "b", "b"}, + expected: false, + }, + { + name: "Different with duplicates in different order", + slice1: []string{"a", "b", "b"}, + slice2: []string{"a", "c", "b"}, + expected: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, ElementsEqual(tc.slice1, tc.slice2)) + }) + } +} diff --git a/utils/xsc/analyticsmetrics_test.go b/utils/xsc/analyticsmetrics_test.go index 81c713f9e..27c0c990f 100644 --- a/utils/xsc/analyticsmetrics_test.go +++ b/utils/xsc/analyticsmetrics_test.go @@ -189,7 +189,7 @@ func TestCreateFinalizedEvent(t *testing.T) { }, { name: "Scan failed no findings.", - auditResults: &results.SecurityCommandResults{ResultsMetaData: results.ResultsMetaData{MultiScanId: "msi", StartTime: time}, Targets: []*results.TargetResults{{Errors: []error{errors.New("an error")}}}}, + auditResults: &results.SecurityCommandResults{ResultsMetaData: results.ResultsMetaData{MultiScanId: "msi", StartTime: time}, Targets: []*results.TargetResults{{TargetErrors: []results.SkippableError{{ActualError: errors.New("an error"), Skip: false}}}}}, expected: xscservices.XscAnalyticsGeneralEventFinalize{ XscAnalyticsBasicGeneralEvent: xscservices.XscAnalyticsBasicGeneralEvent{TotalFindings: 0, EventStatus: xscservices.Failed}, }, @@ -238,7 +238,7 @@ func getDummyContentForGeneralEvent(withJas, withErr, withResultContext bool) *r } if withErr { - scanResults.Errors = []error{errors.New("an error")} + scanResults.TargetErrors = []results.SkippableError{{ActualError: errors.New("an error"), Skip: false}} } if withResultContext { diff --git a/utils/xsc/configprofile_test.go b/utils/xsc/configprofile_test.go index 941bc9366..a57cf9def 100644 --- a/utils/xsc/configprofile_test.go +++ b/utils/xsc/configprofile_test.go @@ -87,7 +87,6 @@ func getComparisonConfigProfile() *services.ConfigProfile { ProfileName: "default-profile", GeneralConfig: services.GeneralConfig{ ScannersDownloadPath: "https://repo.example.com/releases", - GeneralExcludePatterns: []string{"*.log*", "*.tmp*"}, FailUponAnyScannerError: true, }, FrogbotConfig: services.FrogbotConfig{