From d9b82319c7bf04d6cbfe860d1d15ed29c7318e37 Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 20 Apr 2026 11:16:20 +0300 Subject: [PATCH 01/12] SAST Changed Files Mode --- commands/audit/audit.go | 2 + go.mod | 3 +- go.sum | 4 +- jas/runner/jasrunner.go | 3 +- jas/sast/sastscanner.go | 35 ++++++++++-- jas/sast/sastscanner_test.go | 107 +++++++++++++++++++++++++++++++++-- 6 files changed, 139 insertions(+), 15 deletions(-) diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 305fb8429..c8a91e229 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -15,6 +15,7 @@ import ( "github.com/jfrog/jfrog-cli-security/jas" "github.com/jfrog/jfrog-cli-security/jas/applicability" "github.com/jfrog/jfrog-cli-security/jas/runner" + "github.com/jfrog/jfrog-cli-security/jas/sast" "github.com/jfrog/jfrog-cli-security/jas/secrets" "github.com/jfrog/jfrog-cli-security/policy" "github.com/jfrog/jfrog-cli-security/policy/enforcer" @@ -739,6 +740,7 @@ func createJasScansTask(auditParallelRunner *utils.SecurityParallelRunner, scanR ApplicableScanType: applicability.ApplicabilityScannerType, SignedDescriptions: getSignedDescriptions(auditParams.OutputFormat()), SastRules: auditParams.SastRules(), + SastChangedFiles: sast.SastChangedFilesFromGitContext(scanResults.GitContext), ScanResults: targetResult, TargetCount: len(scanResults.Targets), TargetOutputDir: auditParams.scanResultsOutputDir, diff --git a/go.mod b/go.mod index ce4f06670..537f8d75d 100644 --- a/go.mod +++ b/go.mod @@ -151,7 +151,8 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect ) -// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go master +// attiasas:add_chagned_files_to_git_ctx +replace github.com/jfrog/jfrog-client-go => github.com/attiasas/jfrog-client-go v0.0.0-20260420064147-858bbdf9ec5b // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 master diff --git a/go.sum b/go.sum index 80c6afe52..17beb3485 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI 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= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/attiasas/jfrog-client-go v0.0.0-20260420064147-858bbdf9ec5b h1:JGfUd7uoxR6VcYtEzYPW7vFkA6VJ6wnL4mAkrcxEkHM= +github.com/attiasas/jfrog-client-go v0.0.0-20260420064147-858bbdf9ec5b/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U= github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -173,8 +175,6 @@ github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260408205330-fb3f40fbcd22 h1:X github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260408205330-fb3f40fbcd22/go.mod h1:KSJZO+tguFpGG4TE2Ut2rmOk1j03RrqHQ7E33FrsEt4= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260402104745-7a0bc2c11d63 h1:rvEiuETYgy7VbQFmf1QeYTcG0Sp4Lr+1QgrVQzLV58Q= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260402104745-7a0bc2c11d63/go.mod h1:RLLUO+oGDq88e5DPtP/KK2sVgMF32OuoRdVMxSFfb30= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260401130923-f5a15b584a0d h1:H9orUmxbClfYcVwP3yefNJxc6XJIvZp0k3fSBaMOOCc= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260401130923-f5a15b584a0d/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U= 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= diff --git a/jas/runner/jasrunner.go b/jas/runner/jasrunner.go index c88692450..531c59265 100644 --- a/jas/runner/jasrunner.go +++ b/jas/runner/jasrunner.go @@ -34,6 +34,7 @@ type JasRunnerParams struct { ScansToPerform []utils.SubScanType // Diff mode flags SourceResultsToCompare *results.TargetResults + SastChangedFiles []string DiffMode bool // Secret scan flags SecretsScanType secrets.SecretsScanType @@ -175,7 +176,7 @@ func runSastScan(params *JasRunnerParams) parallel.TaskFunc { defer func() { params.Runner.JasScannersWg.Done() }() - vulnerabilitiesResults, violationsResults, err := sast.RunSastScan(params.Scanner, params.Module, params.SignedDescriptions, params.SastRules, params.TargetCount, threadId, getSourceRunsToCompare(params, jasutils.Sast)...) + vulnerabilitiesResults, violationsResults, err := sast.RunSastScan(params.Scanner, params.Module, params.SignedDescriptions, params.SastRules, params.SastChangedFiles, params.TargetCount, threadId, getSourceRunsToCompare(params, jasutils.Sast)...) 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 diff --git a/jas/sast/sastscanner.go b/jas/sast/sastscanner.go index 974c027a4..ca5c235d6 100644 --- a/jas/sast/sastscanner.go +++ b/jas/sast/sastscanner.go @@ -2,6 +2,7 @@ package sast import ( "fmt" + "os" "path/filepath" "time" @@ -11,6 +12,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" "github.com/jfrog/jfrog-client-go/utils/log" + xscservices "github.com/jfrog/jfrog-client-go/xsc/services" "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" "golang.org/x/exp/maps" ) @@ -19,10 +21,26 @@ const ( sastScannerType = "sast" sastScanCommand = "zd" sastDocsUrlSuffix = "sast-1" + + sastChangedFilesModeEnvVar = "JAS_SAST_CHANGED_FILES_MODE" ) +// SastChangedFilesFromGitContext returns gitCtx.ChangedFiles when sastChangedFilesModeEnvVar is "true", +// gitCtx is non-nil, and ChangedFiles is non-empty; otherwise nil. +func SastChangedFilesFromGitContext(gitCtx *xscservices.XscGitInfoContext) []string { + if gitCtx == nil || os.Getenv(sastChangedFilesModeEnvVar) != "true" { + return nil + } + if len(gitCtx.ChangedFiles) == 0 { + return nil + } + return gitCtx.ChangedFiles +} + type SastScanManager struct { - scanner *jas.JasScanner + scanner *jas.JasScanner + + sastChangedFiles []string signedDescriptions bool sastRules string @@ -31,12 +49,12 @@ type SastScanManager struct { resultsFileName string } -func RunSastScan(scanner *jas.JasScanner, module jfrogappsconfig.Module, signedDescriptions bool, sastRules string, targetCount, threadId int, resultsToCompare ...*sarif.Run) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { +func RunSastScan(scanner *jas.JasScanner, module jfrogappsconfig.Module, signedDescriptions bool, sastRules string, sastChangedFiles []string, targetCount, threadId int, resultsToCompare ...*sarif.Run) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { var scannerTempDir string if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.Sast.String(), threadId); err != nil { return } - sastScanManager, err := newSastScanManager(scanner, scannerTempDir, signedDescriptions, sastRules, resultsToCompare...) + sastScanManager, err := newSastScanManager(scanner, scannerTempDir, signedDescriptions, sastRules, sastChangedFiles, resultsToCompare...) if err != nil { return } @@ -49,11 +67,12 @@ func RunSastScan(scanner *jas.JasScanner, module jfrogappsconfig.Module, signedD return } -func newSastScanManager(scanner *jas.JasScanner, scannerTempDir string, signedDescriptions bool, sastRules string, resultsToCompare ...*sarif.Run) (manager *SastScanManager, err error) { +func newSastScanManager(scanner *jas.JasScanner, scannerTempDir string, signedDescriptions bool, sastRules string, sastChangedFiles []string, resultsToCompare ...*sarif.Run) (manager *SastScanManager, err error) { manager = &SastScanManager{ scanner: scanner, signedDescriptions: signedDescriptions, sastRules: sastRules, + sastChangedFiles: sastChangedFiles, configFileName: filepath.Join(scannerTempDir, "config.yaml"), resultsFileName: filepath.Join(scannerTempDir, "results.sarif"), } @@ -69,7 +88,7 @@ func newSastScanManager(scanner *jas.JasScanner, scannerTempDir string, signedDe } func (ssm *SastScanManager) Run(module jfrogappsconfig.Module) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { - if err = ssm.createConfigFile(module, ssm.signedDescriptions, ssm.scanner.ScannersExclusions.SastExcludePatterns, ssm.scanner.Exclusions...); err != nil { + if err = ssm.createConfigFile(module, ssm.signedDescriptions, ssm.sastChangedFiles, ssm.scanner.ScannersExclusions.SastExcludePatterns, ssm.scanner.Exclusions...); err != nil { return } if err = ssm.runAnalyzerManager(filepath.Dir(ssm.scanner.AnalyzerManager.AnalyzerManagerFullPath)); err != nil { @@ -104,7 +123,7 @@ type sastParameters struct { SignedDescriptions bool `yaml:"signed_descriptions,omitempty"` } -func (ssm *SastScanManager) createConfigFile(module jfrogappsconfig.Module, signedDescriptions bool, centralConfigExclusions []string, exclusions ...string) error { +func (ssm *SastScanManager) createConfigFile(module jfrogappsconfig.Module, signedDescriptions bool, sastChangedFiles []string, centralConfigExclusions []string, exclusions ...string) error { sastScanner := module.Scanners.Sast if sastScanner == nil { sastScanner = &jfrogappsconfig.SastScanner{} @@ -113,6 +132,10 @@ func (ssm *SastScanManager) createConfigFile(module jfrogappsconfig.Module, sign if err != nil { return err } + if len(sastChangedFiles) > 0 { + log.Debug(fmt.Sprintf("Using SAST Changed Files mode with %d changed files", len(sastChangedFiles))) + roots = sastChangedFiles + } configFileContent := sastScanConfig{ Scans: []scanConfiguration{ { diff --git a/jas/sast/sastscanner_test.go b/jas/sast/sastscanner_test.go index 7b51e9f02..c0fbc9a9b 100644 --- a/jas/sast/sastscanner_test.go +++ b/jas/sast/sastscanner_test.go @@ -1,9 +1,12 @@ package sast import ( + "gopkg.in/yaml.v3" + "os" "path/filepath" "testing" + jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -11,6 +14,7 @@ import ( coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + xscservices "github.com/jfrog/jfrog-client-go/xsc/services" "github.com/jfrog/jfrog-cli-security/jas" "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" @@ -23,7 +27,7 @@ func TestNewSastScanManager(t *testing.T) { jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{"currentDir"}) assert.NoError(t, err) // Act - sastScanManager, err := newSastScanManager(scanner, "temoDirPath", true, "") + sastScanManager, err := newSastScanManager(scanner, "tempDirPath", true, "", nil) assert.NoError(t, err) // Assert @@ -47,7 +51,7 @@ func TestNewSastScanManagerWithFilesToCompare(t *testing.T) { scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.Secrets.String(), 0) require.NoError(t, err) - sastScanManager, err := newSastScanManager(scanner, scannerTempDir, false, "", sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyResult("test-markdown", "test-msg", "test-rule-id", "note"))) + sastScanManager, err := newSastScanManager(scanner, scannerTempDir, false, "", nil, 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 @@ -62,7 +66,7 @@ func TestSastParseResults_EmptyResults(t *testing.T) { assert.NoError(t, err) // Arrange - sastScanManager, err := newSastScanManager(scanner, "temoDirPath", true, "") + sastScanManager, err := newSastScanManager(scanner, "tempDirPath", true, "", nil) assert.NoError(t, err) sastScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "sast-scan", "no-violations.sarif") @@ -85,7 +89,7 @@ func TestSastParseResults_ResultsContainIacViolations(t *testing.T) { jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) assert.NoError(t, err) // Arrange - sastScanManager, err := newSastScanManager(scanner, "temoDirPath", false, "") + sastScanManager, err := newSastScanManager(scanner, "tempDirPath", false, "", nil) assert.NoError(t, err) sastScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "sast-scan", "contains-sast-violations.sarif") @@ -198,9 +202,102 @@ func TestSastRules(t *testing.T) { scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.Sast.String(), 0) require.NoError(t, err) - sastScanManager, err := newSastScanManager(scanner, scannerTempDir, false, "test-rules.json") + sastScanManager, err := newSastScanManager(scanner, scannerTempDir, false, "test-rules.json", nil) require.NoError(t, err) assert.Equal(t, "test-rules.json", sastScanManager.sastRules) assert.Equal(t, filepath.Join(scannerTempDir, "config.yaml"), sastScanManager.configFileName) assert.Equal(t, filepath.Join(scannerTempDir, "results.sarif"), sastScanManager.resultsFileName) } + +func TestSastChangedFilesFromGitContext(t *testing.T) { + gitCtxWithFiles := &xscservices.XscGitInfoContext{} + gitCtxWithFiles.ChangedFiles = []string{"pkg/a.go", "pkg/b.go"} + + tests := []struct { + name string + envValue string + gitCtx *xscservices.XscGitInfoContext + wantNil bool + want []string + }{ + {name: "nil_context", envValue: "true", gitCtx: nil, wantNil: true}, + {name: "env_false", envValue: "false", gitCtx: gitCtxWithFiles, wantNil: true}, + {name: "env_empty", envValue: "", gitCtx: gitCtxWithFiles, wantNil: true}, + {name: "empty_changed_files_env_true", envValue: "true", gitCtx: &xscservices.XscGitInfoContext{}, wantNil: true}, + {name: "returns_changed_files", envValue: "true", gitCtx: gitCtxWithFiles, want: []string{"pkg/a.go", "pkg/b.go"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(ChangedFilesModeEnvVar, tt.envValue) + got := SastChangedFilesFromGitContext(tt.gitCtx) + if tt.wantNil { + assert.Nil(t, got) + } else { + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestCreateConfigFile_ChangedFilesModeRoots(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.Sast.String(), 0) + require.NoError(t, err) + + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + require.NoError(t, err) + module := jfrogAppsConfigForTest.Modules[0] + sastScanner := module.Scanners.Sast + if sastScanner == nil { + sastScanner = &jfrogappsconfig.SastScanner{} + } + expectedDefaultRoots, err := jas.GetSourceRoots(module, &sastScanner.Scanner) + require.NoError(t, err) + + changed := []string{"src/a.go", "src/b.go"} + ssm, err := newSastScanManager(scanner, scannerTempDir, false, "", changed) + require.NoError(t, err) + + type yamlCfg struct { + Scans []struct { + Roots []string `yaml:"roots,omitempty"` + } `yaml:"scans,omitempty"` + } + + t.Run("env_true_uses_changed_files_as_roots", func(t *testing.T) { + t.Setenv(ChangedFilesModeEnvVar, "true") + require.NoError(t, ssm.createConfigFile(module, false, changed, nil)) + data, err := os.ReadFile(ssm.configFileName) + require.NoError(t, err) + var cfg yamlCfg + require.NoError(t, yaml.Unmarshal(data, &cfg)) + require.Len(t, cfg.Scans, 1) + assert.Equal(t, changed, cfg.Scans[0].Roots) + }) + + t.Run("env_false_ignores_changed_files", func(t *testing.T) { + t.Setenv(ChangedFilesModeEnvVar, "false") + require.NoError(t, ssm.createConfigFile(module, false, changed, nil)) + data, err := os.ReadFile(ssm.configFileName) + require.NoError(t, err) + var cfg yamlCfg + require.NoError(t, yaml.Unmarshal(data, &cfg)) + require.Len(t, cfg.Scans, 1) + assert.Equal(t, expectedDefaultRoots, cfg.Scans[0].Roots) + }) + + t.Run("env_true_nil_changed_files_uses_default_roots", func(t *testing.T) { + t.Setenv(ChangedFilesModeEnvVar, "true") + require.NoError(t, ssm.createConfigFile(module, false, nil, nil)) + data, err := os.ReadFile(ssm.configFileName) + require.NoError(t, err) + var cfg yamlCfg + require.NoError(t, yaml.Unmarshal(data, &cfg)) + require.Len(t, cfg.Scans, 1) + assert.Equal(t, expectedDefaultRoots, cfg.Scans[0].Roots) + }) +} From 713bf2d22262e47ef84e29cef0db6d59bdd2bbef Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 20 Apr 2026 11:16:43 +0300 Subject: [PATCH 02/12] only target changes --- commands/audit/audit.go | 2 +- jas/sast/sastscanner.go | 75 ++++++++++++++++++++++++++++++++---- jas/sast/sastscanner_test.go | 74 ++++++++++++++++++++++++++++------- 3 files changed, 129 insertions(+), 22 deletions(-) diff --git a/commands/audit/audit.go b/commands/audit/audit.go index c8a91e229..74f42c9e6 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -740,7 +740,7 @@ func createJasScansTask(auditParallelRunner *utils.SecurityParallelRunner, scanR ApplicableScanType: applicability.ApplicabilityScannerType, SignedDescriptions: getSignedDescriptions(auditParams.OutputFormat()), SastRules: auditParams.SastRules(), - SastChangedFiles: sast.SastChangedFilesFromGitContext(scanResults.GitContext), + SastChangedFiles: sast.SastChangedFilesForTarget(scanResults.GitContext, targetResult.Target, scanResults.GetCommonParentPath()), ScanResults: targetResult, TargetCount: len(scanResults.Targets), TargetOutputDir: auditParams.scanResultsOutputDir, diff --git a/jas/sast/sastscanner.go b/jas/sast/sastscanner.go index ca5c235d6..97498e6df 100644 --- a/jas/sast/sastscanner.go +++ b/jas/sast/sastscanner.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" @@ -22,19 +23,79 @@ const ( sastScanCommand = "zd" sastDocsUrlSuffix = "sast-1" - sastChangedFilesModeEnvVar = "JAS_SAST_CHANGED_FILES_MODE" + // ChangedFilesModeEnvVar enables using GitContext changed files (scoped per target) as SAST scan roots. + ChangedFilesModeEnvVar = "JAS_SAST_CHANGED_FILES_MODE" ) -// SastChangedFilesFromGitContext returns gitCtx.ChangedFiles when sastChangedFilesModeEnvVar is "true", -// gitCtx is non-nil, and ChangedFiles is non-empty; otherwise nil. -func SastChangedFilesFromGitContext(gitCtx *xscservices.XscGitInfoContext) []string { - if gitCtx == nil || os.Getenv(sastChangedFilesModeEnvVar) != "true" { +// SastChangedFilesForTarget returns absolute paths of git changed files that belong to targetPath +// (relative to commonParent), when ChangedFilesModeEnvVar is "true". Returns nil if nothing matches +// or if gitCtx, commonParent, or targetPath are unusable. +func SastChangedFilesForTarget(gitCtx *xscservices.XscGitInfoContext, targetPath, commonParent string) []string { + if gitCtx == nil || os.Getenv(ChangedFilesModeEnvVar) != "true" { return nil } if len(gitCtx.ChangedFiles) == 0 { return nil } - return gitCtx.ChangedFiles + if strings.TrimSpace(commonParent) == "" || strings.TrimSpace(targetPath) == "" { + return nil + } + commonAbs, err := filepath.Abs(filepath.Clean(commonParent)) + if err != nil { + return nil + } + targetRel := filepath.ToSlash(utils.GetRelativePath(targetPath, commonParent)) + + var out []string + for _, cf := range gitCtx.ChangedFiles { + cfSlash, ok := normalizeRepoRelativeChangedPath(commonAbs, cf) + if !ok { + continue + } + if !changedFileBelongsToTarget(targetRel, cfSlash) { + continue + } + joined := filepath.Join(commonAbs, filepath.FromSlash(cfSlash)) + absPath, err := filepath.Abs(filepath.Clean(joined)) + if err != nil { + continue + } + out = append(out, absPath) + } + if len(out) == 0 { + return nil + } + return out +} + +func normalizeRepoRelativeChangedPath(commonAbs, cf string) (slashPath string, ok bool) { + cf = strings.TrimSpace(cf) + if cf == "" { + return "", false + } + if filepath.IsAbs(cf) { + cleaned := filepath.Clean(cf) + r, err := filepath.Rel(commonAbs, cleaned) + if err != nil { + return "", false + } + r = filepath.ToSlash(filepath.Clean(r)) + if r == ".." || strings.HasPrefix(r, "../") { + return "", false + } + return r, true + } + return filepath.ToSlash(filepath.Clean(cf)), true +} + +func changedFileBelongsToTarget(targetRel, cfSlash string) bool { + if targetRel == "" { + return true + } + if cfSlash == targetRel { + return true + } + return strings.HasPrefix(cfSlash, targetRel+"/") } type SastScanManager struct { @@ -132,7 +193,7 @@ func (ssm *SastScanManager) createConfigFile(module jfrogappsconfig.Module, sign if err != nil { return err } - if len(sastChangedFiles) > 0 { + if len(sastChangedFiles) > 0 && os.Getenv(ChangedFilesModeEnvVar) == "true" { log.Debug(fmt.Sprintf("Using SAST Changed Files mode with %d changed files", len(sastChangedFiles))) roots = sastChangedFiles } diff --git a/jas/sast/sastscanner_test.go b/jas/sast/sastscanner_test.go index c0fbc9a9b..913f57eae 100644 --- a/jas/sast/sastscanner_test.go +++ b/jas/sast/sastscanner_test.go @@ -209,31 +209,77 @@ func TestSastRules(t *testing.T) { assert.Equal(t, filepath.Join(scannerTempDir, "results.sarif"), sastScanManager.resultsFileName) } -func TestSastChangedFilesFromGitContext(t *testing.T) { +func TestSastChangedFilesForTarget(t *testing.T) { + base := t.TempDir() + modA := filepath.Join(base, "modA") + modB := filepath.Join(base, "modB") + require.NoError(t, os.MkdirAll(modA, 0o755)) + require.NoError(t, os.MkdirAll(modB, 0o755)) + gitCtxWithFiles := &xscservices.XscGitInfoContext{} - gitCtxWithFiles.ChangedFiles = []string{"pkg/a.go", "pkg/b.go"} + gitCtxWithFiles.ChangedFiles = []string{"modA/a.go", "modA/b.go", "modB/x.go"} tests := []struct { - name string - envValue string - gitCtx *xscservices.XscGitInfoContext - wantNil bool - want []string + name string + envValue string + gitCtx *xscservices.XscGitInfoContext + targetPath string + commonParent string + wantNil bool + want []string }{ - {name: "nil_context", envValue: "true", gitCtx: nil, wantNil: true}, - {name: "env_false", envValue: "false", gitCtx: gitCtxWithFiles, wantNil: true}, - {name: "env_empty", envValue: "", gitCtx: gitCtxWithFiles, wantNil: true}, - {name: "empty_changed_files_env_true", envValue: "true", gitCtx: &xscservices.XscGitInfoContext{}, wantNil: true}, - {name: "returns_changed_files", envValue: "true", gitCtx: gitCtxWithFiles, want: []string{"pkg/a.go", "pkg/b.go"}}, + {name: "nil_context", envValue: "true", gitCtx: nil, targetPath: base, commonParent: base, wantNil: true}, + {name: "env_false", envValue: "false", gitCtx: gitCtxWithFiles, targetPath: modA, commonParent: base, wantNil: true}, + {name: "env_empty", envValue: "", gitCtx: gitCtxWithFiles, targetPath: modA, commonParent: base, wantNil: true}, + {name: "empty_changed_files_env_true", envValue: "true", gitCtx: &xscservices.XscGitInfoContext{}, targetPath: modA, commonParent: base, wantNil: true}, + {name: "empty_common_parent", envValue: "true", gitCtx: gitCtxWithFiles, targetPath: modA, commonParent: "", wantNil: true}, + {name: "empty_target_path", envValue: "true", gitCtx: gitCtxWithFiles, targetPath: "", commonParent: base, wantNil: true}, + { + name: "target_is_common_parent_returns_all_as_abs", + envValue: "true", + gitCtx: gitCtxWithFiles, + targetPath: base, + commonParent: base, + want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go"), filepath.Join(base, "modB", "x.go")}, + }, + { + name: "filters_to_modA_only", + envValue: "true", + gitCtx: gitCtxWithFiles, + targetPath: modA, + commonParent: base, + want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go")}, + }, + { + name: "prefix_foo_does_not_match_foobar", + envValue: "true", + gitCtx: &xscservices.XscGitInfoContext{GitDiffContext: xscservices.GitDiffContext{ChangedFiles: []string{"foo/x.go", "foobar/y.go"}}}, + targetPath: filepath.Join(base, "foo"), + commonParent: base, + want: []string{filepath.Join(base, "foo", "x.go")}, + }, + { + name: "absolute_changed_file_under_repo", + envValue: "true", + gitCtx: func() *xscservices.XscGitInfoContext { + c := &xscservices.XscGitInfoContext{} + c.ChangedFiles = []string{filepath.Join(base, "modA", "abs.go")} + return c + }(), + targetPath: modA, + commonParent: base, + want: []string{filepath.Join(base, "modA", "abs.go")}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv(ChangedFilesModeEnvVar, tt.envValue) - got := SastChangedFilesFromGitContext(tt.gitCtx) + got := SastChangedFilesForTarget(tt.gitCtx, tt.targetPath, tt.commonParent) if tt.wantNil { assert.Nil(t, got) } else { - assert.Equal(t, tt.want, got) + require.Len(t, got, len(tt.want)) + assert.ElementsMatch(t, tt.want, got) } }) } From 66219bac54d49aa9355646078653d040fedca7a7 Mon Sep 17 00:00:00 2001 From: attiasas Date: Sun, 26 Apr 2026 14:25:24 +0300 Subject: [PATCH 03/12] update dep --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 537f8d75d..db75db478 100644 --- a/go.mod +++ b/go.mod @@ -152,7 +152,7 @@ require ( ) // attiasas:add_chagned_files_to_git_ctx -replace github.com/jfrog/jfrog-client-go => github.com/attiasas/jfrog-client-go v0.0.0-20260420064147-858bbdf9ec5b +replace github.com/jfrog/jfrog-client-go => github.com/attiasas/jfrog-client-go v0.0.0-20260426111214-788a89d406bb // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 master diff --git a/go.sum b/go.sum index 17beb3485..a5838a35e 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI 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= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/attiasas/jfrog-client-go v0.0.0-20260420064147-858bbdf9ec5b h1:JGfUd7uoxR6VcYtEzYPW7vFkA6VJ6wnL4mAkrcxEkHM= -github.com/attiasas/jfrog-client-go v0.0.0-20260420064147-858bbdf9ec5b/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U= +github.com/attiasas/jfrog-client-go v0.0.0-20260426111214-788a89d406bb h1:gyoSro/3P6DKuLCQ/E+B/hl7S2gtrt3oBgHiLOuWPQA= +github.com/attiasas/jfrog-client-go v0.0.0-20260426111214-788a89d406bb/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U= github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= From e7ba29cbdabfbd64edfb01ea3790fe712dc7d2dd Mon Sep 17 00:00:00 2001 From: attiasas Date: Sun, 26 Apr 2026 15:45:40 +0300 Subject: [PATCH 04/12] add tests and improve --- jas/sast/sastscanner.go | 193 +++++++++++++++++++++-------------- jas/sast/sastscanner_test.go | 142 +++++++++++++++++--------- utils/utils.go | 14 +++ utils/utils_test.go | 30 ++++++ 4 files changed, 255 insertions(+), 124 deletions(-) diff --git a/jas/sast/sastscanner.go b/jas/sast/sastscanner.go index 97498e6df..5d0a24b68 100644 --- a/jas/sast/sastscanner.go +++ b/jas/sast/sastscanner.go @@ -2,16 +2,18 @@ package sast import ( "fmt" - "os" "path/filepath" + "slices" "strings" "time" + "github.com/jfrog/gofrog/datastructures" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" "github.com/jfrog/jfrog-cli-security/jas" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" + clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" xscservices "github.com/jfrog/jfrog-client-go/xsc/services" "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" @@ -27,77 +29,6 @@ const ( ChangedFilesModeEnvVar = "JAS_SAST_CHANGED_FILES_MODE" ) -// SastChangedFilesForTarget returns absolute paths of git changed files that belong to targetPath -// (relative to commonParent), when ChangedFilesModeEnvVar is "true". Returns nil if nothing matches -// or if gitCtx, commonParent, or targetPath are unusable. -func SastChangedFilesForTarget(gitCtx *xscservices.XscGitInfoContext, targetPath, commonParent string) []string { - if gitCtx == nil || os.Getenv(ChangedFilesModeEnvVar) != "true" { - return nil - } - if len(gitCtx.ChangedFiles) == 0 { - return nil - } - if strings.TrimSpace(commonParent) == "" || strings.TrimSpace(targetPath) == "" { - return nil - } - commonAbs, err := filepath.Abs(filepath.Clean(commonParent)) - if err != nil { - return nil - } - targetRel := filepath.ToSlash(utils.GetRelativePath(targetPath, commonParent)) - - var out []string - for _, cf := range gitCtx.ChangedFiles { - cfSlash, ok := normalizeRepoRelativeChangedPath(commonAbs, cf) - if !ok { - continue - } - if !changedFileBelongsToTarget(targetRel, cfSlash) { - continue - } - joined := filepath.Join(commonAbs, filepath.FromSlash(cfSlash)) - absPath, err := filepath.Abs(filepath.Clean(joined)) - if err != nil { - continue - } - out = append(out, absPath) - } - if len(out) == 0 { - return nil - } - return out -} - -func normalizeRepoRelativeChangedPath(commonAbs, cf string) (slashPath string, ok bool) { - cf = strings.TrimSpace(cf) - if cf == "" { - return "", false - } - if filepath.IsAbs(cf) { - cleaned := filepath.Clean(cf) - r, err := filepath.Rel(commonAbs, cleaned) - if err != nil { - return "", false - } - r = filepath.ToSlash(filepath.Clean(r)) - if r == ".." || strings.HasPrefix(r, "../") { - return "", false - } - return r, true - } - return filepath.ToSlash(filepath.Clean(cf)), true -} - -func changedFileBelongsToTarget(targetRel, cfSlash string) bool { - if targetRel == "" { - return true - } - if cfSlash == targetRel { - return true - } - return strings.HasPrefix(cfSlash, targetRel+"/") -} - type SastScanManager struct { scanner *jas.JasScanner @@ -111,6 +42,11 @@ type SastScanManager struct { } func RunSastScan(scanner *jas.JasScanner, module jfrogappsconfig.Module, signedDescriptions bool, sastRules string, sastChangedFiles []string, targetCount, threadId int, resultsToCompare ...*sarif.Run) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { + // In changed-files mode with nothing in scope, do not fall back to a full module scan. Diff mode (baseline compare) must still run. + if utils.IsEnvVarTruthy(ChangedFilesModeEnvVar) && len(sastChangedFiles) == 0 && len(resultsToCompare) == 0 { + log.Info(clientutils.GetLogMsgPrefix(threadId, false) + "SAST changed files mode: no changed files in scope for this target, skipping SAST scan") + return + } var scannerTempDir string if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.Sast.String(), threadId); err != nil { return @@ -193,8 +129,8 @@ func (ssm *SastScanManager) createConfigFile(module jfrogappsconfig.Module, sign if err != nil { return err } - if len(sastChangedFiles) > 0 && os.Getenv(ChangedFilesModeEnvVar) == "true" { - log.Debug(fmt.Sprintf("Using SAST Changed Files mode with %d changed files", len(sastChangedFiles))) + if utils.IsEnvVarTruthy(ChangedFilesModeEnvVar) { + log.Debug(fmt.Sprintf("SAST changed files mode: using %d paths as scan roots", len(sastChangedFiles))) roots = sastChangedFiles } configFileContent := sastScanConfig{ @@ -255,3 +191,112 @@ func getResultLocationStr(result *sarif.Result) string { func getResultId(result *sarif.Result) string { return sarifutils.GetResultRuleId(result) + result.Level + sarifutils.GetResultMsgText(result) + getResultLocationStr(result) } + +// sastChangedFileDropStats counts reasons entries from git were not used as SAST roots. +type sastChangedFileDropStats struct { + invalidPath int + outsideTarget int + absError int + duplicate int +} + +func (s sastChangedFileDropStats) anyDrops() bool { + return s.invalidPath+s.outsideTarget+s.absError+s.duplicate > 0 +} + +// collectSastChangedAbsPaths maps repo-relative (or absolute-under-repo) changed file paths to clean absolute +// paths under commonAbs that belong to targetRel, deduplicating by absolute path. +func collectSastChangedAbsPaths(commonAbs, targetRel string, changedFiles []string) (out []string, stats sastChangedFileDropStats) { + seen := datastructures.MakeSet[string]() + for _, cf := range changedFiles { + cfSlash, ok := normalizeRepoRelativeChangedPath(commonAbs, cf) + if !ok { + stats.invalidPath++ + continue + } + if !changedFileBelongsToTarget(targetRel, cfSlash) { + stats.outsideTarget++ + continue + } + joined := filepath.Join(commonAbs, filepath.FromSlash(cfSlash)) + absPath, err := filepath.Abs(filepath.Clean(joined)) + if err != nil { + stats.absError++ + continue + } + if seen.Exists(absPath) { + stats.duplicate++ + continue + } + seen.Add(absPath) + out = append(out, absPath) + } + return out, stats +} + +// SastChangedFilesForTarget returns absolute paths of git changed files that belong to targetPath +// (relative to commonParent), when ChangedFilesModeEnvVar is truthy (see utils.IsEnvVarTruthy). Returns nil if nothing matches +// or if gitCtx, commonParent, or targetPath are unusable. +func SastChangedFilesForTarget(gitCtx *xscservices.XscGitInfoContext, targetPath, commonParent string) []string { + if gitCtx == nil { + return nil + } + if !utils.IsEnvVarTruthy(ChangedFilesModeEnvVar) { + return nil + } + if len(gitCtx.ChangedFiles) == 0 { + log.Debug("SAST changed files: git context has no changed files; skipping per-file roots") + return nil + } + if strings.TrimSpace(commonParent) == "" || strings.TrimSpace(targetPath) == "" { + log.Debug("SAST changed files: empty common parent or target path; skipping per-file roots") + return nil + } + commonAbs, err := filepath.Abs(filepath.Clean(commonParent)) + if err != nil { + log.Debug(fmt.Sprintf("SAST changed files: could not resolve common parent: %s", err.Error())) + return nil + } + targetRel := filepath.ToSlash(utils.GetRelativePath(targetPath, commonParent)) + inputCount := len(gitCtx.ChangedFiles) + out, stats := collectSastChangedAbsPaths(commonAbs, targetRel, gitCtx.ChangedFiles) + if stats.anyDrops() { + log.Debug(fmt.Sprintf("SAST changed files: kept %d of %d changed-file entries (dropped: %d invalid/unsafe path, %d outside target, %d path resolution error, %d duplicate after normalization)", + len(out), inputCount, stats.invalidPath, stats.outsideTarget, stats.absError, stats.duplicate)) + } + if len(out) == 0 { + return nil + } + slices.Sort(out) + return out +} + +func normalizeRepoRelativeChangedPath(commonAbs, cf string) (slashPath string, ok bool) { + cf = strings.TrimSpace(cf) + if cf == "" { + return "", false + } + if filepath.IsAbs(cf) { + cleaned := filepath.Clean(cf) + r, err := filepath.Rel(commonAbs, cleaned) + if err != nil { + return "", false + } + r = filepath.ToSlash(filepath.Clean(r)) + if r == ".." || strings.HasPrefix(r, "../") { + return "", false + } + return r, true + } + return filepath.ToSlash(filepath.Clean(cf)), true +} + +func changedFileBelongsToTarget(targetRel, cfSlash string) bool { + if targetRel == "" { + return true + } + if cfSlash == targetRel { + return true + } + return strings.HasPrefix(cfSlash, targetRel+"/") +} diff --git a/jas/sast/sastscanner_test.go b/jas/sast/sastscanner_test.go index 913f57eae..ac36adefb 100644 --- a/jas/sast/sastscanner_test.go +++ b/jas/sast/sastscanner_test.go @@ -209,6 +209,13 @@ func TestSastRules(t *testing.T) { assert.Equal(t, filepath.Join(scannerTempDir, "results.sarif"), sastScanManager.resultsFileName) } +// xscGitInfoWithChanged builds an XscGitInfoContext the way the client defines it (GitDiffContext with changed_files). +// Must match the shape expected by SastChangedFilesForTarget in sastscanner.go. +func xscGitInfoWithChanged(t *testing.T, files ...string) *xscservices.XscGitInfoContext { + t.Helper() + return &xscservices.XscGitInfoContext{GitDiffContext: xscservices.GitDiffContext{ChangedFiles: files}} +} + func TestSastChangedFilesForTarget(t *testing.T) { base := t.TempDir() modA := filepath.Join(base, "modA") @@ -216,8 +223,7 @@ func TestSastChangedFilesForTarget(t *testing.T) { require.NoError(t, os.MkdirAll(modA, 0o755)) require.NoError(t, os.MkdirAll(modB, 0o755)) - gitCtxWithFiles := &xscservices.XscGitInfoContext{} - gitCtxWithFiles.ChangedFiles = []string{"modA/a.go", "modA/b.go", "modB/x.go"} + threeFiles := xscGitInfoWithChanged(t, "modA/a.go", "modA/b.go", "modB/x.go") tests := []struct { name string @@ -225,19 +231,20 @@ func TestSastChangedFilesForTarget(t *testing.T) { gitCtx *xscservices.XscGitInfoContext targetPath string commonParent string - wantNil bool - want []string + // wantEmpty: expect no file roots (nil or empty slice) when mode is off or there is nothing to return. + wantEmpty bool + want []string }{ - {name: "nil_context", envValue: "true", gitCtx: nil, targetPath: base, commonParent: base, wantNil: true}, - {name: "env_false", envValue: "false", gitCtx: gitCtxWithFiles, targetPath: modA, commonParent: base, wantNil: true}, - {name: "env_empty", envValue: "", gitCtx: gitCtxWithFiles, targetPath: modA, commonParent: base, wantNil: true}, - {name: "empty_changed_files_env_true", envValue: "true", gitCtx: &xscservices.XscGitInfoContext{}, targetPath: modA, commonParent: base, wantNil: true}, - {name: "empty_common_parent", envValue: "true", gitCtx: gitCtxWithFiles, targetPath: modA, commonParent: "", wantNil: true}, - {name: "empty_target_path", envValue: "true", gitCtx: gitCtxWithFiles, targetPath: "", commonParent: base, wantNil: true}, + {name: "nil_context", envValue: "true", gitCtx: nil, targetPath: base, commonParent: base, wantEmpty: true}, + {name: "env_false", envValue: "false", gitCtx: threeFiles, targetPath: modA, commonParent: base, wantEmpty: true}, + {name: "env_empty", envValue: "", gitCtx: threeFiles, targetPath: modA, commonParent: base, wantEmpty: true}, + {name: "empty_changed_files_env_true", envValue: "true", gitCtx: xscGitInfoWithChanged(t), targetPath: modA, commonParent: base, wantEmpty: true}, + {name: "empty_common_parent", envValue: "true", gitCtx: threeFiles, targetPath: modA, commonParent: "", wantEmpty: true}, + {name: "empty_target_path", envValue: "true", gitCtx: threeFiles, targetPath: "", commonParent: base, wantEmpty: true}, { name: "target_is_common_parent_returns_all_as_abs", envValue: "true", - gitCtx: gitCtxWithFiles, + gitCtx: threeFiles, targetPath: base, commonParent: base, want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go"), filepath.Join(base, "modB", "x.go")}, @@ -245,7 +252,7 @@ func TestSastChangedFilesForTarget(t *testing.T) { { name: "filters_to_modA_only", envValue: "true", - gitCtx: gitCtxWithFiles, + gitCtx: threeFiles, targetPath: modA, commonParent: base, want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go")}, @@ -259,27 +266,38 @@ func TestSastChangedFilesForTarget(t *testing.T) { want: []string{filepath.Join(base, "foo", "x.go")}, }, { - name: "absolute_changed_file_under_repo", - envValue: "true", - gitCtx: func() *xscservices.XscGitInfoContext { - c := &xscservices.XscGitInfoContext{} - c.ChangedFiles = []string{filepath.Join(base, "modA", "abs.go")} - return c - }(), + name: "absolute_changed_file_under_repo", + envValue: "true", + gitCtx: xscGitInfoWithChanged(t, filepath.Join(base, "modA", "abs.go")), targetPath: modA, commonParent: base, want: []string{filepath.Join(base, "modA", "abs.go")}, }, + { + name: "env_1_enables", + envValue: "1", + gitCtx: threeFiles, + targetPath: modA, + commonParent: base, + want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go")}, + }, + { + name: "deduplicates_same_paths", + envValue: "true", + gitCtx: &xscservices.XscGitInfoContext{GitDiffContext: xscservices.GitDiffContext{ChangedFiles: []string{"modA/a.go", "modA/a.go", "./modA/a.go"}}}, + targetPath: modA, + commonParent: base, + want: []string{filepath.Join(base, "modA", "a.go")}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv(ChangedFilesModeEnvVar, tt.envValue) got := SastChangedFilesForTarget(tt.gitCtx, tt.targetPath, tt.commonParent) - if tt.wantNil { - assert.Nil(t, got) + if tt.wantEmpty { + assert.Empty(t, got, "SastChangedFilesForTarget should not return any paths in this case") } else { - require.Len(t, got, len(tt.want)) - assert.ElementsMatch(t, tt.want, got) + assert.ElementsMatch(t, tt.want, got, "SastChangedFilesForTarget per-target paths (order may be sorted in implementation)") } }) } @@ -313,37 +331,61 @@ func TestCreateConfigFile_ChangedFilesModeRoots(t *testing.T) { Roots []string `yaml:"roots,omitempty"` } `yaml:"scans,omitempty"` } - - t.Run("env_true_uses_changed_files_as_roots", func(t *testing.T) { - t.Setenv(ChangedFilesModeEnvVar, "true") - require.NoError(t, ssm.createConfigFile(module, false, changed, nil)) - data, err := os.ReadFile(ssm.configFileName) - require.NoError(t, err) - var cfg yamlCfg - require.NoError(t, yaml.Unmarshal(data, &cfg)) - require.Len(t, cfg.Scans, 1) - assert.Equal(t, changed, cfg.Scans[0].Roots) - }) - - t.Run("env_false_ignores_changed_files", func(t *testing.T) { - t.Setenv(ChangedFilesModeEnvVar, "false") - require.NoError(t, ssm.createConfigFile(module, false, changed, nil)) + readConfigRoots := func(t *testing.T) []string { + t.Helper() data, err := os.ReadFile(ssm.configFileName) require.NoError(t, err) var cfg yamlCfg require.NoError(t, yaml.Unmarshal(data, &cfg)) require.Len(t, cfg.Scans, 1) - assert.Equal(t, expectedDefaultRoots, cfg.Scans[0].Roots) - }) + return cfg.Scans[0].Roots + } - t.Run("env_true_nil_changed_files_uses_default_roots", func(t *testing.T) { - t.Setenv(ChangedFilesModeEnvVar, "true") - require.NoError(t, ssm.createConfigFile(module, false, nil, nil)) - data, err := os.ReadFile(ssm.configFileName) - require.NoError(t, err) - var cfg yamlCfg - require.NoError(t, yaml.Unmarshal(data, &cfg)) - require.Len(t, cfg.Scans, 1) - assert.Equal(t, expectedDefaultRoots, cfg.Scans[0].Roots) - }) + for _, tc := range []struct { + name string + env string + // sastForCall is the slice passed to createConfigFile; nil to pass nil. + sastForCall []string + want []string + emptyRoots bool + }{ + { + name: "env_true_uses_changed_files_as_roots", + env: "true", + sastForCall: changed, + want: changed, + }, + { + name: "env_1_uses_changed_files_as_roots", + env: "1", + sastForCall: changed, + want: changed, + }, + { + name: "env_false_ignores_changed_files", + env: "false", + sastForCall: changed, + want: expectedDefaultRoots, + }, + { + // In changed-files mode, do not use full module roots; RunSastScan skips the analyzer with no diff baseline. + name: "env_true_no_changed_file_list_uses_no_module_roots", + env: "true", + sastForCall: nil, + emptyRoots: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + // createConfigFile assigns roots to sastChangedFiles whenever this env is truthy (sastscanner.go); + // pass-through nil/empty is intentional in changed-files mode to avoid a full module scan. + t.Setenv(ChangedFilesModeEnvVar, tc.env) + require.NoError(t, ssm.createConfigFile(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") + } else { + assert.ElementsMatch(t, tc.want, got) + } + }) + } } diff --git a/utils/utils.go b/utils/utils.go index 467895696..40fa08f77 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -10,6 +10,7 @@ import ( "io" "os" "path/filepath" + "strconv" "strings" "sync" @@ -166,6 +167,19 @@ func IsCI() bool { return strings.ToLower(os.Getenv(coreutils.CI)) == "true" } +// IsEnvVarTruthy reports whether the named environment variable is set to a truthy value. +// Unset or empty (after trim) is false. Values accepted by strconv.ParseBool (e.g. "1", "t", "true", case-insensitive) are true. +func IsEnvVarTruthy(name string) bool { + v := strings.TrimSpace(os.Getenv(name)) + if v == "" { + return false + } + if b, err := strconv.ParseBool(v); err == nil { + return b + } + return false +} + // UniqueIntersection returns a new slice of strings that contains elements from both input slices without duplicates func UniqueIntersection[T comparable](arr []T, others ...T) []T { uniqueSet := datastructures.MakeSetFromElements(arr...) diff --git a/utils/utils_test.go b/utils/utils_test.go index 7629efe11..47f713679 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -1,6 +1,7 @@ package utils import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -163,3 +164,32 @@ func TestMergeMaps(t *testing.T) { }) } } + +func TestIsEnvVarTruthy(t *testing.T) { + const envName = "JFROG_CLI_TEST_IS_ENV_TRUTHY" + tests := []struct { + set string + want bool + }{ + {"", false}, + {" ", false}, + {"true", true}, + {"True", true}, + {"TRUE", true}, + {"1", true}, + {"0", false}, + {"false", false}, + {"f", false}, + {"t", true}, + {"y", false}, + {"yes", false}, + {"on", false}, + {"maybe", false}, + } + for _, tc := range tests { + t.Run(fmt.Sprintf("%q", tc.set), func(t *testing.T) { + t.Setenv(envName, tc.set) + assert.Equal(t, tc.want, IsEnvVarTruthy(envName), "value %q", tc.set) + }) + } +} From 189482c6b1c19cd693fa0e333e212c3af9d96960 Mon Sep 17 00:00:00 2001 From: attiasas Date: Sun, 26 Apr 2026 18:12:22 +0300 Subject: [PATCH 05/12] CR changes --- jas/sast/sastscanner.go | 9 +++++---- utils/utils.go | 14 -------------- utils/utils_test.go | 30 ------------------------------ 3 files changed, 5 insertions(+), 48 deletions(-) diff --git a/jas/sast/sastscanner.go b/jas/sast/sastscanner.go index 5d0a24b68..68ed856b0 100644 --- a/jas/sast/sastscanner.go +++ b/jas/sast/sastscanner.go @@ -2,6 +2,7 @@ package sast import ( "fmt" + "os" "path/filepath" "slices" "strings" @@ -26,7 +27,7 @@ const ( sastDocsUrlSuffix = "sast-1" // ChangedFilesModeEnvVar enables using GitContext changed files (scoped per target) as SAST scan roots. - ChangedFilesModeEnvVar = "JAS_SAST_CHANGED_FILES_MODE" + ChangedFilesModeEnvVar = "JFROG_SAST_CHANGED_FILES_MODE" ) type SastScanManager struct { @@ -43,7 +44,7 @@ type SastScanManager struct { func RunSastScan(scanner *jas.JasScanner, module jfrogappsconfig.Module, signedDescriptions bool, sastRules string, sastChangedFiles []string, targetCount, threadId int, resultsToCompare ...*sarif.Run) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { // In changed-files mode with nothing in scope, do not fall back to a full module scan. Diff mode (baseline compare) must still run. - if utils.IsEnvVarTruthy(ChangedFilesModeEnvVar) && len(sastChangedFiles) == 0 && len(resultsToCompare) == 0 { + if strings.ToLower(strings.TrimSpace(os.Getenv(ChangedFilesModeEnvVar))) == "true" && len(sastChangedFiles) == 0 && len(resultsToCompare) == 0 { log.Info(clientutils.GetLogMsgPrefix(threadId, false) + "SAST changed files mode: no changed files in scope for this target, skipping SAST scan") return } @@ -129,7 +130,7 @@ func (ssm *SastScanManager) createConfigFile(module jfrogappsconfig.Module, sign if err != nil { return err } - if utils.IsEnvVarTruthy(ChangedFilesModeEnvVar) { + if strings.ToLower(strings.TrimSpace(os.Getenv(ChangedFilesModeEnvVar))) == "true" { log.Debug(fmt.Sprintf("SAST changed files mode: using %d paths as scan roots", len(sastChangedFiles))) roots = sastChangedFiles } @@ -241,7 +242,7 @@ func SastChangedFilesForTarget(gitCtx *xscservices.XscGitInfoContext, targetPath if gitCtx == nil { return nil } - if !utils.IsEnvVarTruthy(ChangedFilesModeEnvVar) { + if strings.ToLower(strings.TrimSpace(os.Getenv(ChangedFilesModeEnvVar))) != "true" { return nil } if len(gitCtx.ChangedFiles) == 0 { diff --git a/utils/utils.go b/utils/utils.go index 40fa08f77..467895696 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -10,7 +10,6 @@ import ( "io" "os" "path/filepath" - "strconv" "strings" "sync" @@ -167,19 +166,6 @@ func IsCI() bool { return strings.ToLower(os.Getenv(coreutils.CI)) == "true" } -// IsEnvVarTruthy reports whether the named environment variable is set to a truthy value. -// Unset or empty (after trim) is false. Values accepted by strconv.ParseBool (e.g. "1", "t", "true", case-insensitive) are true. -func IsEnvVarTruthy(name string) bool { - v := strings.TrimSpace(os.Getenv(name)) - if v == "" { - return false - } - if b, err := strconv.ParseBool(v); err == nil { - return b - } - return false -} - // UniqueIntersection returns a new slice of strings that contains elements from both input slices without duplicates func UniqueIntersection[T comparable](arr []T, others ...T) []T { uniqueSet := datastructures.MakeSetFromElements(arr...) diff --git a/utils/utils_test.go b/utils/utils_test.go index 47f713679..7629efe11 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -1,7 +1,6 @@ package utils import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -164,32 +163,3 @@ func TestMergeMaps(t *testing.T) { }) } } - -func TestIsEnvVarTruthy(t *testing.T) { - const envName = "JFROG_CLI_TEST_IS_ENV_TRUTHY" - tests := []struct { - set string - want bool - }{ - {"", false}, - {" ", false}, - {"true", true}, - {"True", true}, - {"TRUE", true}, - {"1", true}, - {"0", false}, - {"false", false}, - {"f", false}, - {"t", true}, - {"y", false}, - {"yes", false}, - {"on", false}, - {"maybe", false}, - } - for _, tc := range tests { - t.Run(fmt.Sprintf("%q", tc.set), func(t *testing.T) { - t.Setenv(envName, tc.set) - assert.Equal(t, tc.want, IsEnvVarTruthy(envName), "value %q", tc.set) - }) - } -} From 28750a7ba79436646847bddb144b50118a91a6f8 Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 27 Apr 2026 10:36:43 +0300 Subject: [PATCH 06/12] add option to pass var to control mode --- cli/scancommands.go | 2 ++ commands/audit/audit.go | 4 +++- commands/audit/auditparams.go | 14 +++++++------- jas/runner/jasrunner.go | 20 +++++++++++++++++--- jas/sast/sastscanner.go | 34 +++++++++++++++++++++++++--------- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/cli/scancommands.go b/cli/scancommands.go index dd6b2e568..7b0a6fb9c 100644 --- a/cli/scancommands.go +++ b/cli/scancommands.go @@ -22,6 +22,7 @@ import ( flags "github.com/jfrog/jfrog-cli-security/cli/docs" auditSpecificDocs "github.com/jfrog/jfrog-cli-security/cli/docs/auditspecific" enrichDocs "github.com/jfrog/jfrog-cli-security/cli/docs/enrich" + "github.com/jfrog/jfrog-cli-security/jas/sast" maliciousScanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/maliciousscan" mcpDocs "github.com/jfrog/jfrog-cli-security/cli/docs/mcp" @@ -507,6 +508,7 @@ func AuditCmd(c *components.Context) error { auditCmd.SetSastRules(sastRulesFile) } + auditCmd.SetSastChangedFilesMode(sast.IsChangedFilesMode(false)) threads, err := pluginsCommon.GetThreadsCount(c) if err != nil { diff --git a/commands/audit/audit.go b/commands/audit/audit.go index a4f502fbe..c2e249e2b 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -267,7 +267,8 @@ func (auditCmd *AuditCommand) Run() (err error) { SetGitContext(auditCmd.GitContext()). SetThirdPartyApplicabilityScan(auditCmd.thirdPartyApplicabilityScan). SetThreads(auditCmd.Threads). - SetScansResultsOutputDir(auditCmd.scanResultsOutputDir).SetStartTime(startTime).SetMultiScanId(multiScanId) + SetScansResultsOutputDir(auditCmd.scanResultsOutputDir).SetStartTime(startTime).SetMultiScanId(multiScanId). + SetSastChangedFilesMode(sast.IsChangedFilesMode(false)).SetSastRules(auditCmd.sastRules) auditParams.SetIsRecursiveScan(isRecursiveScan).SetExclusions(auditCmd.Exclusions()) auditResults := RunAudit(auditParams) @@ -741,6 +742,7 @@ func createJasScansTask(auditParallelRunner *utils.SecurityParallelRunner, scanR ApplicableScanType: applicability.ApplicabilityScannerType, SignedDescriptions: getSignedDescriptions(auditParams.OutputFormat()), SastRules: auditParams.SastRules(), + SastChangedFilesMode: auditParams.SastChangedFilesMode(), SastChangedFiles: sast.SastChangedFilesForTarget(scanResults.GitContext, targetResult.Target, scanResults.GetCommonParentPath()), ScanResults: targetResult, TargetCount: len(scanResults.Targets), diff --git a/commands/audit/auditparams.go b/commands/audit/auditparams.go index c75da1790..1c02bf504 100644 --- a/commands/audit/auditparams.go +++ b/commands/audit/auditparams.go @@ -43,9 +43,9 @@ type AuditParams struct { violationGenerator policy.PolicyHandler sastRules string // Diff mode, scan only the files affected by the diff. - diffMode bool - filesToScan []string - resultsToCompare *results.SecurityCommandResults + diffMode bool + sastChangedFilesMode bool + resultsToCompare *results.SecurityCommandResults } func NewAuditParams() *AuditParams { @@ -258,13 +258,13 @@ func (params *AuditParams) ToXrayScanGraphParams() (scanGraphParams scangraph.Sc return } -func (params *AuditParams) SetFilesToScan(filesToScan []string) *AuditParams { - params.filesToScan = filesToScan +func (params *AuditParams) SetSastChangedFilesMode(sastChangedFilesMode bool) *AuditParams { + params.sastChangedFilesMode = sastChangedFilesMode return params } -func (params *AuditParams) FilesToScan() []string { - return params.filesToScan +func (params *AuditParams) SastChangedFilesMode() bool { + return params.sastChangedFilesMode } func (params *AuditParams) SetResultsToCompare(resultsToCompare *results.SecurityCommandResults) *AuditParams { diff --git a/jas/runner/jasrunner.go b/jas/runner/jasrunner.go index 531c59265..e81657c26 100644 --- a/jas/runner/jasrunner.go +++ b/jas/runner/jasrunner.go @@ -43,8 +43,9 @@ type JasRunnerParams struct { ApplicableScanType applicability.ApplicabilityScanType ThirdPartyApplicabilityScan bool // SAST scan flags - SignedDescriptions bool - SastRules string + SastChangedFilesMode bool + SignedDescriptions bool + SastRules string // Outputs TargetCount int ScanResults *results.TargetResults @@ -176,7 +177,20 @@ func runSastScan(params *JasRunnerParams) parallel.TaskFunc { defer func() { params.Runner.JasScannersWg.Done() }() - vulnerabilitiesResults, violationsResults, err := sast.RunSastScan(params.Scanner, params.Module, params.SignedDescriptions, params.SastRules, params.SastChangedFiles, params.TargetCount, threadId, getSourceRunsToCompare(params, jasutils.Sast)...) + vulnerabilitiesResults, violationsResults, err := sast.RunSastScan( + sast.SastScanParams{ + Module: params.Module, + SignedDescriptions: params.SignedDescriptions, + SastRules: params.SastRules, + TargetCount: params.TargetCount, + ThreadId: threadId, + SastChangedFiles: params.SastChangedFiles, + ChangedFilesMode: params.SastChangedFilesMode, + // TODO: maybe not needed if changed files mode is enabled + ResultsToCompare: getSourceRunsToCompare(params, jasutils.Sast), + }, + params.Scanner, + ) 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 diff --git a/jas/sast/sastscanner.go b/jas/sast/sastscanner.go index 68ed856b0..df60fb5d1 100644 --- a/jas/sast/sastscanner.go +++ b/jas/sast/sastscanner.go @@ -37,31 +37,47 @@ type SastScanManager struct { signedDescriptions bool sastRules string + changedFilesMode bool + resultsToCompareFileName string configFileName string resultsFileName string } -func RunSastScan(scanner *jas.JasScanner, module jfrogappsconfig.Module, signedDescriptions bool, sastRules string, sastChangedFiles []string, targetCount, threadId int, resultsToCompare ...*sarif.Run) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { - // In changed-files mode with nothing in scope, do not fall back to a full module scan. Diff mode (baseline compare) must still run. - if strings.ToLower(strings.TrimSpace(os.Getenv(ChangedFilesModeEnvVar))) == "true" && len(sastChangedFiles) == 0 && len(resultsToCompare) == 0 { - log.Info(clientutils.GetLogMsgPrefix(threadId, false) + "SAST changed files mode: no changed files in scope for this target, skipping SAST scan") +type SastScanParams struct { + Module jfrogappsconfig.Module + SignedDescriptions bool + SastRules string + TargetCount int + ThreadId int + SastChangedFiles []string + ChangedFilesMode bool + ResultsToCompare []*sarif.Run +} + +func IsChangedFilesMode(changedFilesMode bool) bool { + return changedFilesMode || strings.ToLower(strings.TrimSpace(os.Getenv(ChangedFilesModeEnvVar))) == "true" +} + +func RunSastScan(params SastScanParams, scanner *jas.JasScanner) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { + if params.ChangedFilesMode && len(params.SastChangedFiles) == 0 { + log.Info(clientutils.GetLogMsgPrefix(params.ThreadId, false) + "SAST changed files mode: no changed files in scope for this target, skipping SAST scan") return } var scannerTempDir string - if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.Sast.String(), threadId); err != nil { + if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.Sast.String(), params.ThreadId); err != nil { return } - sastScanManager, err := newSastScanManager(scanner, scannerTempDir, signedDescriptions, sastRules, sastChangedFiles, resultsToCompare...) + sastScanManager, err := newSastScanManager(scanner, scannerTempDir, params.SignedDescriptions, params.SastRules, params.SastChangedFiles, params.ResultsToCompare...) if err != nil { return } startTime := time.Now() - log.Info(jas.GetStartJasScanLog(utils.SastScan, threadId, module, targetCount)) - if vulnerabilitiesResults, violationsResults, err = sastScanManager.scanner.Run(sastScanManager, module); err != nil { + log.Info(jas.GetStartJasScanLog(utils.SastScan, params.ThreadId, params.Module, params.TargetCount)) + if vulnerabilitiesResults, violationsResults, err = sastScanManager.scanner.Run(sastScanManager, params.Module); err != nil { return } - log.Info(utils.GetScanFindingsLog(utils.SastScan, sarifutils.GetResultsLocationCount(vulnerabilitiesResults...), startTime, threadId)) + log.Info(utils.GetScanFindingsLog(utils.SastScan, sarifutils.GetResultsLocationCount(vulnerabilitiesResults...), startTime, params.ThreadId)) return } From 772109461a492740affeb346c7d2ef12f1592250 Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 27 Apr 2026 10:51:24 +0300 Subject: [PATCH 07/12] update deps --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5b891ff12..842b9cda2 100644 --- a/go.mod +++ b/go.mod @@ -152,7 +152,7 @@ require ( ) // attiasas:add_chagned_files_to_git_ctx -replace github.com/jfrog/jfrog-client-go => github.com/attiasas/jfrog-client-go v0.0.0-20260426111214-788a89d406bb +replace github.com/jfrog/jfrog-client-go => github.com/attiasas/jfrog-client-go v0.0.0-20260427074859-054f5ce6f99e // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 master diff --git a/go.sum b/go.sum index 3592305d0..ba1220b30 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI 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= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/attiasas/jfrog-client-go v0.0.0-20260426111214-788a89d406bb h1:gyoSro/3P6DKuLCQ/E+B/hl7S2gtrt3oBgHiLOuWPQA= -github.com/attiasas/jfrog-client-go v0.0.0-20260426111214-788a89d406bb/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U= +github.com/attiasas/jfrog-client-go v0.0.0-20260427074859-054f5ce6f99e h1:7B24fuLZ/xbiHfVj1DPZV+HKdpcSxVFjN+HAPV2iYWI= +github.com/attiasas/jfrog-client-go v0.0.0-20260427074859-054f5ce6f99e/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U= github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= From 0aab155354b88e8cabd3d5e01ff403f551a7bd16 Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 27 Apr 2026 11:05:58 +0300 Subject: [PATCH 08/12] update dep --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 842b9cda2..b8852bee3 100644 --- a/go.mod +++ b/go.mod @@ -152,7 +152,7 @@ require ( ) // attiasas:add_chagned_files_to_git_ctx -replace github.com/jfrog/jfrog-client-go => github.com/attiasas/jfrog-client-go v0.0.0-20260427074859-054f5ce6f99e +replace github.com/jfrog/jfrog-client-go => github.com/attiasas/jfrog-client-go v0.0.0-20260427080430-a2f591c32959 // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 master diff --git a/go.sum b/go.sum index ba1220b30..77c64fa35 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI 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= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/attiasas/jfrog-client-go v0.0.0-20260427074859-054f5ce6f99e h1:7B24fuLZ/xbiHfVj1DPZV+HKdpcSxVFjN+HAPV2iYWI= -github.com/attiasas/jfrog-client-go v0.0.0-20260427074859-054f5ce6f99e/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U= +github.com/attiasas/jfrog-client-go v0.0.0-20260427080430-a2f591c32959 h1:n08pdWZvXXp95FDzZbSj23CSWMzUlaHKFYcc53j0+iI= +github.com/attiasas/jfrog-client-go v0.0.0-20260427080430-a2f591c32959/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U= github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= From aee26841d7de6a2be6715d33e13c0b9a873c7a64 Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 27 Apr 2026 13:00:31 +0300 Subject: [PATCH 09/12] fix tests --- commands/audit/audit.go | 2 +- jas/runner/jasrunner.go | 3 +- jas/sast/sastscanner.go | 36 +++++----- jas/sast/sastscanner_test.go | 135 ++++++++++++++++++++--------------- 4 files changed, 101 insertions(+), 75 deletions(-) diff --git a/commands/audit/audit.go b/commands/audit/audit.go index c2e249e2b..48b7e7d67 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -743,7 +743,7 @@ func createJasScansTask(auditParallelRunner *utils.SecurityParallelRunner, scanR SignedDescriptions: getSignedDescriptions(auditParams.OutputFormat()), SastRules: auditParams.SastRules(), SastChangedFilesMode: auditParams.SastChangedFilesMode(), - SastChangedFiles: sast.SastChangedFilesForTarget(scanResults.GitContext, targetResult.Target, scanResults.GetCommonParentPath()), + SastChangedFiles: sast.SastChangedFilesForTarget(auditParams.SastChangedFilesMode(), scanResults.GitContext, targetResult.Target, scanResults.GetCommonParentPath()), ScanResults: targetResult, TargetCount: len(scanResults.Targets), TargetOutputDir: auditParams.scanResultsOutputDir, diff --git a/jas/runner/jasrunner.go b/jas/runner/jasrunner.go index e81657c26..d88e29cb2 100644 --- a/jas/runner/jasrunner.go +++ b/jas/runner/jasrunner.go @@ -186,8 +186,7 @@ func runSastScan(params *JasRunnerParams) parallel.TaskFunc { ThreadId: threadId, SastChangedFiles: params.SastChangedFiles, ChangedFilesMode: params.SastChangedFilesMode, - // TODO: maybe not needed if changed files mode is enabled - ResultsToCompare: getSourceRunsToCompare(params, jasutils.Sast), + ResultsToCompare: getSourceRunsToCompare(params, jasutils.Sast), }, params.Scanner, ) diff --git a/jas/sast/sastscanner.go b/jas/sast/sastscanner.go index df60fb5d1..b52f0a16c 100644 --- a/jas/sast/sastscanner.go +++ b/jas/sast/sastscanner.go @@ -15,6 +15,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" 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" xscservices "github.com/jfrog/jfrog-client-go/xsc/services" "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" @@ -56,11 +57,15 @@ type SastScanParams struct { } func IsChangedFilesMode(changedFilesMode bool) bool { - return changedFilesMode || strings.ToLower(strings.TrimSpace(os.Getenv(ChangedFilesModeEnvVar))) == "true" + if changedFilesMode { + return true + } + v := strings.ToLower(strings.TrimSpace(os.Getenv(ChangedFilesModeEnvVar))) + return v == "true" || v == "1" } func RunSastScan(params SastScanParams, scanner *jas.JasScanner) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { - if params.ChangedFilesMode && len(params.SastChangedFiles) == 0 { + if IsChangedFilesMode(params.ChangedFilesMode) && len(params.SastChangedFiles) == 0 { log.Info(clientutils.GetLogMsgPrefix(params.ThreadId, false) + "SAST changed files mode: no changed files in scope for this target, skipping SAST scan") return } @@ -68,7 +73,7 @@ func RunSastScan(params SastScanParams, scanner *jas.JasScanner) (vulnerabilitie if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.Sast.String(), params.ThreadId); err != nil { return } - sastScanManager, err := newSastScanManager(scanner, scannerTempDir, params.SignedDescriptions, params.SastRules, params.SastChangedFiles, params.ResultsToCompare...) + sastScanManager, err := newSastScanManager(scanner, scannerTempDir, params.SignedDescriptions, params.ChangedFilesMode, params.SastRules, params.SastChangedFiles, params.ResultsToCompare...) if err != nil { return } @@ -81,11 +86,12 @@ func RunSastScan(params SastScanParams, scanner *jas.JasScanner) (vulnerabilitie return } -func newSastScanManager(scanner *jas.JasScanner, scannerTempDir string, signedDescriptions bool, sastRules string, sastChangedFiles []string, resultsToCompare ...*sarif.Run) (manager *SastScanManager, err error) { +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, signedDescriptions: signedDescriptions, sastRules: sastRules, + changedFilesMode: changedFilesMode, sastChangedFiles: sastChangedFiles, configFileName: filepath.Join(scannerTempDir, "config.yaml"), resultsFileName: filepath.Join(scannerTempDir, "results.sarif"), @@ -146,7 +152,7 @@ func (ssm *SastScanManager) createConfigFile(module jfrogappsconfig.Module, sign if err != nil { return err } - if strings.ToLower(strings.TrimSpace(os.Getenv(ChangedFilesModeEnvVar))) == "true" { + if IsChangedFilesMode(ssm.changedFilesMode) { log.Debug(fmt.Sprintf("SAST changed files mode: using %d paths as scan roots", len(sastChangedFiles))) roots = sastChangedFiles } @@ -241,6 +247,10 @@ func collectSastChangedAbsPaths(commonAbs, targetRel string, changedFiles []stri stats.absError++ continue } + if exists, err := fileutils.IsFileExists(absPath, false); err != nil || !exists { + stats.invalidPath++ + continue + } if seen.Exists(absPath) { stats.duplicate++ continue @@ -251,14 +261,11 @@ func collectSastChangedAbsPaths(commonAbs, targetRel string, changedFiles []stri return out, stats } -// SastChangedFilesForTarget returns absolute paths of git changed files that belong to targetPath -// (relative to commonParent), when ChangedFilesModeEnvVar is truthy (see utils.IsEnvVarTruthy). Returns nil if nothing matches -// or if gitCtx, commonParent, or targetPath are unusable. -func SastChangedFilesForTarget(gitCtx *xscservices.XscGitInfoContext, targetPath, commonParent string) []string { - if gitCtx == nil { - return nil - } - if strings.ToLower(strings.TrimSpace(os.Getenv(ChangedFilesModeEnvVar))) != "true" { +// SastChangedFilesForTarget returns absolute paths of changed files under commonParent that belong to targetPath +// (paths from git are repo-relative or absolute under the repo). Only runs when changedFilesMode is true; only paths +// that exist on disk are returned. Returns nil if nothing matches or if gitCtx, commonParent, or targetPath are unusable. +func SastChangedFilesForTarget(changedFilesMode bool, gitCtx *xscservices.XscGitInfoContext, targetPath, commonParent string) []string { + if gitCtx == nil || !changedFilesMode { return nil } if len(gitCtx.ChangedFiles) == 0 { @@ -281,9 +288,6 @@ func SastChangedFilesForTarget(gitCtx *xscservices.XscGitInfoContext, targetPath log.Debug(fmt.Sprintf("SAST changed files: kept %d of %d changed-file entries (dropped: %d invalid/unsafe path, %d outside target, %d path resolution error, %d duplicate after normalization)", len(out), inputCount, stats.invalidPath, stats.outsideTarget, stats.absError, stats.duplicate)) } - if len(out) == 0 { - return nil - } slices.Sort(out) return out } diff --git a/jas/sast/sastscanner_test.go b/jas/sast/sastscanner_test.go index ac36adefb..9621ae213 100644 --- a/jas/sast/sastscanner_test.go +++ b/jas/sast/sastscanner_test.go @@ -27,7 +27,7 @@ func TestNewSastScanManager(t *testing.T) { jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{"currentDir"}) assert.NoError(t, err) // Act - sastScanManager, err := newSastScanManager(scanner, "tempDirPath", true, "", nil) + sastScanManager, err := newSastScanManager(scanner, "tempDirPath", true, false, "", nil) assert.NoError(t, err) // Assert @@ -51,7 +51,7 @@ func TestNewSastScanManagerWithFilesToCompare(t *testing.T) { scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.Secrets.String(), 0) require.NoError(t, err) - sastScanManager, err := newSastScanManager(scanner, scannerTempDir, false, "", nil, sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyResult("test-markdown", "test-msg", "test-rule-id", "note"))) + sastScanManager, err := newSastScanManager(scanner, scannerTempDir, false, false, "", nil, 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 @@ -66,7 +66,7 @@ func TestSastParseResults_EmptyResults(t *testing.T) { assert.NoError(t, err) // Arrange - sastScanManager, err := newSastScanManager(scanner, "tempDirPath", true, "", nil) + sastScanManager, err := newSastScanManager(scanner, "tempDirPath", true, false, "", nil) assert.NoError(t, err) sastScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "sast-scan", "no-violations.sarif") @@ -89,7 +89,7 @@ func TestSastParseResults_ResultsContainIacViolations(t *testing.T) { jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) assert.NoError(t, err) // Arrange - sastScanManager, err := newSastScanManager(scanner, "tempDirPath", false, "", nil) + sastScanManager, err := newSastScanManager(scanner, "tempDirPath", false, false, "", nil) assert.NoError(t, err) sastScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "sast-scan", "contains-sast-violations.sarif") @@ -202,7 +202,7 @@ func TestSastRules(t *testing.T) { scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.Sast.String(), 0) require.NoError(t, err) - sastScanManager, err := newSastScanManager(scanner, scannerTempDir, false, "test-rules.json", nil) + sastScanManager, err := newSastScanManager(scanner, scannerTempDir, false, false, "test-rules.json", nil) require.NoError(t, err) assert.Equal(t, "test-rules.json", sastScanManager.sastRules) assert.Equal(t, filepath.Join(scannerTempDir, "config.yaml"), sastScanManager.configFileName) @@ -222,78 +222,102 @@ func TestSastChangedFilesForTarget(t *testing.T) { modB := filepath.Join(base, "modB") require.NoError(t, os.MkdirAll(modA, 0o755)) require.NoError(t, os.MkdirAll(modB, 0o755)) + // collectSastChangedAbsPaths only keeps paths that exist on disk + for _, rel := range []string{ + "modA/a.go", "modA/b.go", "modB/x.go", "modA/abs.go", "foo/x.go", "foobar/y.go", + } { + p := filepath.Join(base, rel) + require.NoError(t, os.MkdirAll(filepath.Dir(p), 0o755)) + require.NoError(t, os.WriteFile(p, []byte("// test\n"), 0o644)) + } threeFiles := xscGitInfoWithChanged(t, "modA/a.go", "modA/b.go", "modB/x.go") tests := []struct { - name string - envValue string - gitCtx *xscservices.XscGitInfoContext - targetPath string - commonParent string + name string + envValue string + gitCtx *xscservices.XscGitInfoContext + targetPath string + commonParent string + changedFilesMode bool // wantEmpty: expect no file roots (nil or empty slice) when mode is off or there is nothing to return. wantEmpty bool want []string }{ - {name: "nil_context", envValue: "true", gitCtx: nil, targetPath: base, commonParent: base, wantEmpty: true}, - {name: "env_false", envValue: "false", gitCtx: threeFiles, targetPath: modA, commonParent: base, wantEmpty: true}, - {name: "env_empty", envValue: "", gitCtx: threeFiles, targetPath: modA, commonParent: base, wantEmpty: true}, - {name: "empty_changed_files_env_true", envValue: "true", gitCtx: xscGitInfoWithChanged(t), targetPath: modA, commonParent: base, wantEmpty: true}, - {name: "empty_common_parent", envValue: "true", gitCtx: threeFiles, targetPath: modA, commonParent: "", wantEmpty: true}, - {name: "empty_target_path", envValue: "true", gitCtx: threeFiles, targetPath: "", commonParent: base, wantEmpty: true}, + {name: "nil_context", envValue: "true", gitCtx: nil, targetPath: base, commonParent: base, changedFilesMode: true, wantEmpty: true}, + {name: "env_false", envValue: "false", gitCtx: threeFiles, targetPath: modA, commonParent: base, changedFilesMode: false, wantEmpty: true}, + {name: "env_empty", envValue: "", gitCtx: threeFiles, targetPath: modA, commonParent: base, changedFilesMode: false, wantEmpty: true}, + {name: "empty_changed_files_env_true", envValue: "true", gitCtx: xscGitInfoWithChanged(t), targetPath: modA, commonParent: base, changedFilesMode: true, wantEmpty: true}, + {name: "empty_common_parent", envValue: "true", gitCtx: threeFiles, targetPath: modA, commonParent: "", changedFilesMode: true, wantEmpty: true}, + {name: "empty_target_path", envValue: "true", gitCtx: threeFiles, targetPath: "", commonParent: base, changedFilesMode: true, wantEmpty: true}, + { + name: "target_is_common_parent_returns_all_as_abs", + envValue: "true", + gitCtx: threeFiles, + targetPath: base, + commonParent: base, + changedFilesMode: true, + want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go"), filepath.Join(base, "modB", "x.go")}, + }, { - name: "target_is_common_parent_returns_all_as_abs", - envValue: "true", - gitCtx: threeFiles, - targetPath: base, - commonParent: base, - want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go"), filepath.Join(base, "modB", "x.go")}, + name: "filters_to_modA_only", + envValue: "true", + gitCtx: threeFiles, + targetPath: modA, + commonParent: base, + changedFilesMode: true, + want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go")}, }, { - name: "filters_to_modA_only", - envValue: "true", - gitCtx: threeFiles, - targetPath: modA, - commonParent: base, - want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go")}, + name: "prefix_foo_does_not_match_foobar", + envValue: "true", + gitCtx: &xscservices.XscGitInfoContext{GitDiffContext: xscservices.GitDiffContext{ChangedFiles: []string{"foo/x.go", "foobar/y.go"}}}, + targetPath: filepath.Join(base, "foo"), + commonParent: base, + changedFilesMode: true, + want: []string{filepath.Join(base, "foo", "x.go")}, }, { - name: "prefix_foo_does_not_match_foobar", - envValue: "true", - gitCtx: &xscservices.XscGitInfoContext{GitDiffContext: xscservices.GitDiffContext{ChangedFiles: []string{"foo/x.go", "foobar/y.go"}}}, - targetPath: filepath.Join(base, "foo"), - commonParent: base, - want: []string{filepath.Join(base, "foo", "x.go")}, + name: "absolute_changed_file_under_repo", + envValue: "true", + gitCtx: xscGitInfoWithChanged(t, filepath.Join(base, "modA", "abs.go")), + targetPath: modA, + commonParent: base, + changedFilesMode: true, + want: []string{filepath.Join(base, "modA", "abs.go")}, }, { - name: "absolute_changed_file_under_repo", - envValue: "true", - gitCtx: xscGitInfoWithChanged(t, filepath.Join(base, "modA", "abs.go")), - targetPath: modA, - commonParent: base, - want: []string{filepath.Join(base, "modA", "abs.go")}, + name: "env_1_enables", + envValue: "1", + gitCtx: threeFiles, + targetPath: modA, + commonParent: base, + changedFilesMode: true, + want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go")}, }, { - name: "env_1_enables", - envValue: "1", - gitCtx: threeFiles, - targetPath: modA, - commonParent: base, - want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go")}, + name: "deduplicates_same_paths", + envValue: "true", + gitCtx: &xscservices.XscGitInfoContext{GitDiffContext: xscservices.GitDiffContext{ChangedFiles: []string{"modA/a.go", "modA/a.go", "./modA/a.go"}}}, + targetPath: modA, + commonParent: base, + changedFilesMode: true, + want: []string{filepath.Join(base, "modA", "a.go")}, }, { - name: "deduplicates_same_paths", - envValue: "true", - gitCtx: &xscservices.XscGitInfoContext{GitDiffContext: xscservices.GitDiffContext{ChangedFiles: []string{"modA/a.go", "modA/a.go", "./modA/a.go"}}}, - targetPath: modA, - commonParent: base, - want: []string{filepath.Join(base, "modA", "a.go")}, + name: "changed_files_mode_off", + envValue: "false", + gitCtx: threeFiles, + targetPath: modA, + commonParent: base, + changedFilesMode: false, + wantEmpty: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Setenv(ChangedFilesModeEnvVar, tt.envValue) - got := SastChangedFilesForTarget(tt.gitCtx, tt.targetPath, tt.commonParent) + got := SastChangedFilesForTarget(tt.changedFilesMode, tt.gitCtx, tt.targetPath, tt.commonParent) if tt.wantEmpty { assert.Empty(t, got, "SastChangedFilesForTarget should not return any paths in this case") } else { @@ -323,7 +347,7 @@ func TestCreateConfigFile_ChangedFilesModeRoots(t *testing.T) { require.NoError(t, err) changed := []string{"src/a.go", "src/b.go"} - ssm, err := newSastScanManager(scanner, scannerTempDir, false, "", changed) + ssm, err := newSastScanManager(scanner, scannerTempDir, false, false, "", nil) require.NoError(t, err) type yamlCfg struct { @@ -376,8 +400,7 @@ func TestCreateConfigFile_ChangedFilesModeRoots(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - // createConfigFile assigns roots to sastChangedFiles whenever this env is truthy (sastscanner.go); - // pass-through nil/empty is intentional in changed-files mode to avoid a full module scan. + // createConfigFile uses IsChangedFilesMode: env JFROG_SAST_CHANGED_FILES_MODE or manager flag; see sastscanner.go t.Setenv(ChangedFilesModeEnvVar, tc.env) require.NoError(t, ssm.createConfigFile(module, false, tc.sastForCall, nil)) got := readConfigRoots(t) From 239d9ffd9099c73d2e3f4c5636ef856189197043 Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 27 Apr 2026 14:50:37 +0300 Subject: [PATCH 10/12] add logs when skip --- jas/sast/sastscanner.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jas/sast/sastscanner.go b/jas/sast/sastscanner.go index b52f0a16c..bd77e932f 100644 --- a/jas/sast/sastscanner.go +++ b/jas/sast/sastscanner.go @@ -234,24 +234,29 @@ func collectSastChangedAbsPaths(commonAbs, targetRel string, changedFiles []stri for _, cf := range changedFiles { cfSlash, ok := normalizeRepoRelativeChangedPath(commonAbs, cf) if !ok { + log.Verbose(fmt.Sprintf("SAST changed files: invalid path: %s", cf)) stats.invalidPath++ continue } if !changedFileBelongsToTarget(targetRel, cfSlash) { + log.Verbose(fmt.Sprintf("SAST changed files: outside target: %s", cf)) stats.outsideTarget++ continue } joined := filepath.Join(commonAbs, filepath.FromSlash(cfSlash)) absPath, err := filepath.Abs(filepath.Clean(joined)) if err != nil { + log.Verbose(fmt.Sprintf("SAST changed files: absolute path error: %s", err.Error())) stats.absError++ continue } if exists, err := fileutils.IsFileExists(absPath, false); err != nil || !exists { + log.Verbose(fmt.Sprintf("SAST changed files: file does not exist: %s", absPath)) stats.invalidPath++ continue } if seen.Exists(absPath) { + log.Verbose(fmt.Sprintf("SAST changed files: duplicate path: %s", absPath)) stats.duplicate++ continue } From 939ad75fcce75c87ee876899efd6a2a641beac35 Mon Sep 17 00:00:00 2001 From: attiasas Date: Tue, 28 Apr 2026 10:25:19 +0300 Subject: [PATCH 11/12] update deps --- go.mod | 9 ++++----- go.sum | 12 ++++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index b8852bee3..38d8adfb4 100644 --- a/go.mod +++ b/go.mod @@ -15,9 +15,9 @@ require ( github.com/jfrog/froggit-go v1.21.1 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.20260416104146-471c3f71ce61 - github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260420010255-65b7a8d432af - github.com/jfrog/jfrog-client-go v1.55.1-0.20260416101832-c47c1246283b + github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260423195010-d7aa2c437305 + github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260427010241-873f53d940b3 + github.com/jfrog/jfrog-client-go v1.55.1-0.20260428070955-750b933dc5c7 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 @@ -151,8 +151,7 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect ) -// attiasas:add_chagned_files_to_git_ctx -replace github.com/jfrog/jfrog-client-go => github.com/attiasas/jfrog-client-go v0.0.0-20260427080430-a2f591c32959 +// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go master // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 master diff --git a/go.sum b/go.sum index 77c64fa35..35e80e618 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI 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= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/attiasas/jfrog-client-go v0.0.0-20260427080430-a2f591c32959 h1:n08pdWZvXXp95FDzZbSj23CSWMzUlaHKFYcc53j0+iI= -github.com/attiasas/jfrog-client-go v0.0.0-20260427080430-a2f591c32959/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U= github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -171,10 +169,12 @@ 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.20260416104146-471c3f71ce61 h1:cES0xZ9ABOY/f3nq+3ETiVPgzjs7tEpHFzstc8h3W2U= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260416104146-471c3f71ce61/go.mod h1:6QJFQvde/CLnFeIIFOvm/6QuQr8OT1QWiTJAkQ+1Mnc= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260420010255-65b7a8d432af h1:TGYTFW5egYMD1MhLtuLbYwB2vn+KRlVZLk+9uLVjKyM= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260420010255-65b7a8d432af/go.mod h1:qpD7einonjqskDTEyqeG3NzAbZO6se0s0Pet0ObBQ3I= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260423195010-d7aa2c437305 h1:wSoVNwbZ2Scm/q2MEfcf+vCUuq41wejk3+rlgnF56jE= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260423195010-d7aa2c437305/go.mod h1:6QJFQvde/CLnFeIIFOvm/6QuQr8OT1QWiTJAkQ+1Mnc= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260427010241-873f53d940b3 h1:LdLQQmhOMUfU+3x7wbtB7kY/Dd2LXKHz7CCUpHWn7uM= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260427010241-873f53d940b3/go.mod h1:qpD7einonjqskDTEyqeG3NzAbZO6se0s0Pet0ObBQ3I= +github.com/jfrog/jfrog-client-go v1.55.1-0.20260428070955-750b933dc5c7 h1:MvHnFczVntYB/USj7/RRANvdWbTUcwEvXcIGr7lOyTc= +github.com/jfrog/jfrog-client-go v1.55.1-0.20260428070955-750b933dc5c7/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U= 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= From 7a26f907f3627c864908908b45fac223a3108332 Mon Sep 17 00:00:00 2001 From: attiasas Date: Tue, 28 Apr 2026 11:42:19 +0300 Subject: [PATCH 12/12] remove env var, fix tests --- cli/scancommands.go | 2 - commands/audit/audit.go | 2 +- jas/sast/sastscanner.go | 16 +------- jas/sast/sastscanner_test.go | 75 ++++++++++++------------------------ scans_test.go | 6 +-- 5 files changed, 30 insertions(+), 71 deletions(-) diff --git a/cli/scancommands.go b/cli/scancommands.go index 7b0a6fb9c..dd6b2e568 100644 --- a/cli/scancommands.go +++ b/cli/scancommands.go @@ -22,7 +22,6 @@ import ( flags "github.com/jfrog/jfrog-cli-security/cli/docs" auditSpecificDocs "github.com/jfrog/jfrog-cli-security/cli/docs/auditspecific" enrichDocs "github.com/jfrog/jfrog-cli-security/cli/docs/enrich" - "github.com/jfrog/jfrog-cli-security/jas/sast" maliciousScanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/maliciousscan" mcpDocs "github.com/jfrog/jfrog-cli-security/cli/docs/mcp" @@ -508,7 +507,6 @@ func AuditCmd(c *components.Context) error { auditCmd.SetSastRules(sastRulesFile) } - auditCmd.SetSastChangedFilesMode(sast.IsChangedFilesMode(false)) threads, err := pluginsCommon.GetThreadsCount(c) if err != nil { diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 778df007e..dbf7b6cfe 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -268,7 +268,7 @@ func (auditCmd *AuditCommand) Run() (err error) { SetThirdPartyApplicabilityScan(auditCmd.thirdPartyApplicabilityScan). SetThreads(auditCmd.Threads). SetScansResultsOutputDir(auditCmd.scanResultsOutputDir).SetStartTime(startTime).SetMultiScanId(multiScanId). - SetSastChangedFilesMode(sast.IsChangedFilesMode(false)).SetSastRules(auditCmd.sastRules) + SetSastChangedFilesMode(auditCmd.sastChangedFilesMode).SetSastRules(auditCmd.sastRules) auditParams.SetIsRecursiveScan(isRecursiveScan).SetExclusions(auditCmd.Exclusions()) auditResults := RunAudit(auditParams) diff --git a/jas/sast/sastscanner.go b/jas/sast/sastscanner.go index bd77e932f..fc6179155 100644 --- a/jas/sast/sastscanner.go +++ b/jas/sast/sastscanner.go @@ -2,7 +2,6 @@ package sast import ( "fmt" - "os" "path/filepath" "slices" "strings" @@ -26,9 +25,6 @@ const ( sastScannerType = "sast" sastScanCommand = "zd" sastDocsUrlSuffix = "sast-1" - - // ChangedFilesModeEnvVar enables using GitContext changed files (scoped per target) as SAST scan roots. - ChangedFilesModeEnvVar = "JFROG_SAST_CHANGED_FILES_MODE" ) type SastScanManager struct { @@ -56,16 +52,8 @@ type SastScanParams struct { ResultsToCompare []*sarif.Run } -func IsChangedFilesMode(changedFilesMode bool) bool { - if changedFilesMode { - return true - } - v := strings.ToLower(strings.TrimSpace(os.Getenv(ChangedFilesModeEnvVar))) - return v == "true" || v == "1" -} - func RunSastScan(params SastScanParams, scanner *jas.JasScanner) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { - if IsChangedFilesMode(params.ChangedFilesMode) && len(params.SastChangedFiles) == 0 { + if params.ChangedFilesMode && len(params.SastChangedFiles) == 0 { log.Info(clientutils.GetLogMsgPrefix(params.ThreadId, false) + "SAST changed files mode: no changed files in scope for this target, skipping SAST scan") return } @@ -152,7 +140,7 @@ func (ssm *SastScanManager) createConfigFile(module jfrogappsconfig.Module, sign if err != nil { return err } - if IsChangedFilesMode(ssm.changedFilesMode) { + if ssm.changedFilesMode { log.Debug(fmt.Sprintf("SAST changed files mode: using %d paths as scan roots", len(sastChangedFiles))) roots = sastChangedFiles } diff --git a/jas/sast/sastscanner_test.go b/jas/sast/sastscanner_test.go index 9621ae213..5d16a2d48 100644 --- a/jas/sast/sastscanner_test.go +++ b/jas/sast/sastscanner_test.go @@ -235,7 +235,6 @@ func TestSastChangedFilesForTarget(t *testing.T) { tests := []struct { name string - envValue string gitCtx *xscservices.XscGitInfoContext targetPath string commonParent string @@ -244,15 +243,13 @@ func TestSastChangedFilesForTarget(t *testing.T) { wantEmpty bool want []string }{ - {name: "nil_context", envValue: "true", gitCtx: nil, targetPath: base, commonParent: base, changedFilesMode: true, wantEmpty: true}, - {name: "env_false", envValue: "false", gitCtx: threeFiles, targetPath: modA, commonParent: base, changedFilesMode: false, wantEmpty: true}, - {name: "env_empty", envValue: "", gitCtx: threeFiles, targetPath: modA, commonParent: base, changedFilesMode: false, wantEmpty: true}, - {name: "empty_changed_files_env_true", envValue: "true", gitCtx: xscGitInfoWithChanged(t), targetPath: modA, commonParent: base, changedFilesMode: true, wantEmpty: true}, - {name: "empty_common_parent", envValue: "true", gitCtx: threeFiles, targetPath: modA, commonParent: "", changedFilesMode: true, wantEmpty: true}, - {name: "empty_target_path", envValue: "true", gitCtx: threeFiles, targetPath: "", commonParent: base, changedFilesMode: true, wantEmpty: true}, + {name: "nil_context", gitCtx: nil, targetPath: base, commonParent: base, changedFilesMode: true, wantEmpty: true}, + {name: "changed_files_mode_off", gitCtx: threeFiles, targetPath: modA, commonParent: base, changedFilesMode: false, wantEmpty: true}, + {name: "empty_changed_files", gitCtx: xscGitInfoWithChanged(t), targetPath: modA, commonParent: base, changedFilesMode: true, wantEmpty: true}, + {name: "empty_common_parent", gitCtx: threeFiles, targetPath: modA, commonParent: "", changedFilesMode: true, wantEmpty: true}, + {name: "empty_target_path", gitCtx: threeFiles, targetPath: "", commonParent: base, changedFilesMode: true, wantEmpty: true}, { name: "target_is_common_parent_returns_all_as_abs", - envValue: "true", gitCtx: threeFiles, targetPath: base, commonParent: base, @@ -261,7 +258,6 @@ func TestSastChangedFilesForTarget(t *testing.T) { }, { name: "filters_to_modA_only", - envValue: "true", gitCtx: threeFiles, targetPath: modA, commonParent: base, @@ -270,7 +266,6 @@ func TestSastChangedFilesForTarget(t *testing.T) { }, { name: "prefix_foo_does_not_match_foobar", - envValue: "true", gitCtx: &xscservices.XscGitInfoContext{GitDiffContext: xscservices.GitDiffContext{ChangedFiles: []string{"foo/x.go", "foobar/y.go"}}}, targetPath: filepath.Join(base, "foo"), commonParent: base, @@ -279,44 +274,23 @@ func TestSastChangedFilesForTarget(t *testing.T) { }, { name: "absolute_changed_file_under_repo", - envValue: "true", gitCtx: xscGitInfoWithChanged(t, filepath.Join(base, "modA", "abs.go")), targetPath: modA, commonParent: base, changedFilesMode: true, want: []string{filepath.Join(base, "modA", "abs.go")}, }, - { - name: "env_1_enables", - envValue: "1", - gitCtx: threeFiles, - targetPath: modA, - commonParent: base, - changedFilesMode: true, - want: []string{filepath.Join(base, "modA", "a.go"), filepath.Join(base, "modA", "b.go")}, - }, { name: "deduplicates_same_paths", - envValue: "true", gitCtx: &xscservices.XscGitInfoContext{GitDiffContext: xscservices.GitDiffContext{ChangedFiles: []string{"modA/a.go", "modA/a.go", "./modA/a.go"}}}, targetPath: modA, commonParent: base, changedFilesMode: true, want: []string{filepath.Join(base, "modA", "a.go")}, }, - { - name: "changed_files_mode_off", - envValue: "false", - gitCtx: threeFiles, - targetPath: modA, - commonParent: base, - changedFilesMode: false, - wantEmpty: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv(ChangedFilesModeEnvVar, tt.envValue) got := SastChangedFilesForTarget(tt.changedFilesMode, tt.gitCtx, tt.targetPath, tt.commonParent) if tt.wantEmpty { assert.Empty(t, got, "SastChangedFilesForTarget should not return any paths in this case") @@ -366,42 +340,41 @@ func TestCreateConfigFile_ChangedFilesModeRoots(t *testing.T) { } for _, tc := range []struct { - name string - env string + name string + changedFilesMode bool // sastForCall is the slice passed to createConfigFile; nil to pass nil. sastForCall []string want []string emptyRoots bool }{ { - name: "env_true_uses_changed_files_as_roots", - env: "true", - sastForCall: changed, - want: changed, + name: "env_true_uses_changed_files_as_roots", + changedFilesMode: true, + sastForCall: changed, + want: changed, }, { - name: "env_1_uses_changed_files_as_roots", - env: "1", - sastForCall: changed, - want: changed, + name: "env_1_uses_changed_files_as_roots", + changedFilesMode: true, + sastForCall: changed, + want: changed, }, { - name: "env_false_ignores_changed_files", - env: "false", - sastForCall: changed, - want: expectedDefaultRoots, + name: "env_false_ignores_changed_files", + changedFilesMode: false, + sastForCall: changed, + want: expectedDefaultRoots, }, { // In changed-files mode, do not use full module roots; RunSastScan skips the analyzer with no diff baseline. - name: "env_true_no_changed_file_list_uses_no_module_roots", - env: "true", - sastForCall: nil, - emptyRoots: true, + name: "env_true_no_changed_file_list_uses_no_module_roots", + changedFilesMode: true, + sastForCall: nil, + emptyRoots: true, }, } { t.Run(tc.name, func(t *testing.T) { - // createConfigFile uses IsChangedFilesMode: env JFROG_SAST_CHANGED_FILES_MODE or manager flag; see sastscanner.go - t.Setenv(ChangedFilesModeEnvVar, tc.env) + ssm.changedFilesMode = tc.changedFilesMode require.NoError(t, ssm.createConfigFile(module, false, tc.sastForCall, nil)) got := readConfigRoots(t) if tc.emptyRoots { diff --git a/scans_test.go b/scans_test.go index f492efbff..f9fe653a7 100644 --- a/scans_test.go +++ b/scans_test.go @@ -209,12 +209,12 @@ func TestXrayBinaryScanSelectiveScan(t *testing.T) { subScans: []utils.SubScanType{utils.ScaScan, utils.ContextualAnalysisScan}, withoutCa: false, validate: func(t *testing.T, issueCount validations.ValidationCountActualValues) { - // Expect only SCA vulnerabilities + // Expect SCA vulnerabilities and contextual analysis statuses assert.GreaterOrEqual(t, issueCount.ScaVulnerabilities, 17, "SCA vulnerabilities count mismatch - should be 17 or more") + assert.Equal(t, 3, issueCount.ApplicableVulnerabilities, "Applicable vulnerabilities count mismatch - should be 3") + assert.Equal(t, 3, issueCount.UndeterminedVulnerabilities, "Undetermined vulnerabilities count mismatch - should be 3") // All other vulnerability types should be 0 assert.Equal(t, 0, issueCount.SecretsVulnerabilities, "Secrets vulnerabilities count mismatch - should be 0") - assert.Equal(t, 2, issueCount.ApplicableVulnerabilities, "Applicable vulnerabilities count mismatch - should be 2") - assert.Equal(t, 3, issueCount.UndeterminedVulnerabilities, "Undetermined vulnerabilities count mismatch - should be 3") }, }, {