From cd479f26e8b872b3ed5ab0685eed6a4561343ccb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:06:13 +0000 Subject: [PATCH 01/17] feat: add exclude_test_libraries option to reduce SBOM noise from test source paths Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/b3aafaea-fba4-42f2-8079-0e3257f3b677 Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- config.toml | 1 + features/scans/syft/const.go | 20 ++++++++---- features/scans/syft/factory.go | 9 ++++-- features/scans/syft/factory_test.go | 18 +++++++++++ features/scans/syft/service.go | 27 ++++++++++------ features/scans/syft/service_test.go | 50 +++++++++++++++++++++++++---- loader/dto.go | 8 +++++ 7 files changed, 108 insertions(+), 25 deletions(-) diff --git a/config.toml b/config.toml index ed8b350..44e475d 100644 --- a/config.toml +++ b/config.toml @@ -9,6 +9,7 @@ platform = "Dockerfile" [grype] ignore_states = "not-fixed,unknown,wont-fix" transitive_libraries = false +# exclude_test_libraries = false # set to true to skip test source directories (e.g. src/test/) from the SBOM scan [opengrep] exclude = ["tests/**"] diff --git a/features/scans/syft/const.go b/features/scans/syft/const.go index de841b2..0b17d0b 100644 --- a/features/scans/syft/const.go +++ b/features/scans/syft/const.go @@ -7,10 +7,15 @@ const ( ) const ( - scanArgument = "scan" - configArgument = "-c" - outputArgument = "--output" - quietArgument = "-q" + scanArgument = "scan" + configArgument = "-c" + outputArgument = "--output" + quietArgument = "-q" + excludeArgument = "--exclude" +) + +const ( + testSourcePattern = "**/src/test/**" ) const ( @@ -20,9 +25,10 @@ const ( ) const ( - logInfoCommandLine = "Command line invoked [%s]" - logInfoTransitiveLibraries = "Transitive libraries resolution is enabled. This may significantly increase scan time." - logErrorDirectoryNotFound = "Cannot find directory [%s]" + logInfoCommandLine = "Command line invoked [%s]" + logInfoTransitiveLibraries = "Transitive libraries resolution is enabled. This may significantly increase scan time." + logInfoExcludeTestLibraries = "Test source directory exclusion is enabled. Paths matching **/src/test/** will be skipped." + logErrorDirectoryNotFound = "Cannot find directory [%s]" ) const ( diff --git a/features/scans/syft/factory.go b/features/scans/syft/factory.go index accbba2..cbfc85a 100644 --- a/features/scans/syft/factory.go +++ b/features/scans/syft/factory.go @@ -7,12 +7,15 @@ import ( // GetSyftService constructs and returns a ScanServiceImpl for the Syft SBOM generator // using the provided loader configuration. When a Grype configuration is present, -// its TransitiveLibraries flag is forwarded to the Syft service to control whether -// transitive Java dependencies are resolved from Maven Central during SBOM generation. +// its TransitiveLibraries and ExcludeTestLibraries flags are forwarded to the Syft +// service to control whether transitive Java dependencies are resolved from Maven +// Central and whether test source directories are excluded during SBOM generation. func GetSyftService(config loader.Config) interfaces.ScanServiceImpl { transitiveLibraries := false + excludeTestLibraries := false if config.Grype != nil { transitiveLibraries = config.Grype.TransitiveLibraries + excludeTestLibraries = config.Grype.ExcludeTestLibraries } - return newSyftService(config.Path, transitiveLibraries, config.Proxy.ToEnv()) + return newSyftService(config.Path, transitiveLibraries, excludeTestLibraries, config.Proxy.ToEnv()) } diff --git a/features/scans/syft/factory_test.go b/features/scans/syft/factory_test.go index 767081a..4e5da75 100644 --- a/features/scans/syft/factory_test.go +++ b/features/scans/syft/factory_test.go @@ -32,4 +32,22 @@ func TestGetSyftService(t *testing.T) { assert.NotNil(t, service) assert.True(t, ok) }) + + t.Run("Should use excludeTestLibraries from grype config when false", func(t *testing.T) { + service := GetSyftService(loader.Config{Grype: &loader.Grype{ExcludeTestLibraries: false}}) + svc, ok := service.(*SyftServiceImpl) + + assert.NotNil(t, service) + assert.True(t, ok) + assert.False(t, svc.excludeTestLibraries) + }) + + t.Run("Should use excludeTestLibraries from grype config when true", func(t *testing.T) { + service := GetSyftService(loader.Config{Grype: &loader.Grype{ExcludeTestLibraries: true}}) + svc, ok := service.(*SyftServiceImpl) + + assert.NotNil(t, service) + assert.True(t, ok) + assert.True(t, svc.excludeTestLibraries) + }) } diff --git a/features/scans/syft/service.go b/features/scans/syft/service.go index 37b027b..2d0bddc 100644 --- a/features/scans/syft/service.go +++ b/features/scans/syft/service.go @@ -20,23 +20,27 @@ type execRunner func(binaryPath string, dirPath string, args []string, stdout io // SyftServiceImpl implements ScanServiceImpl for the Syft SBOM generator. type SyftServiceImpl struct { - path string - transitiveLibraries bool - proxyEnv []string - runner execRunner + path string + transitiveLibraries bool + excludeTestLibraries bool + proxyEnv []string + runner execRunner } // newSyftService builds a SyftServiceImpl from the scan path, resolving it // relative to the SCAN_DIR environment variable. transitiveLibraries controls // whether Syft resolves transitive Java dependencies from Maven Central. +// excludeTestLibraries controls whether test source directories are excluded +// from the Syft filesystem scan (e.g. **/src/test/**). // proxyEnv is an optional list of "KEY=VALUE" proxy environment variable entries // (see loader.Proxy.ToEnv) forwarded to the Syft process. -func newSyftService(path string, transitiveLibraries bool, proxyEnv []string) interfaces.ScanServiceImpl { +func newSyftService(path string, transitiveLibraries bool, excludeTestLibraries bool, proxyEnv []string) interfaces.ScanServiceImpl { return &SyftServiceImpl{ - path: fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], path), - transitiveLibraries: transitiveLibraries, - proxyEnv: proxyEnv, - runner: exec.Wrap, + path: fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], path), + transitiveLibraries: transitiveLibraries, + excludeTestLibraries: excludeTestLibraries, + proxyEnv: proxyEnv, + runner: exec.Wrap, } } @@ -61,6 +65,11 @@ func (s *SyftServiceImpl) Start() (bool, error) { logger.Info(logInfoTransitiveLibraries) } + if s.excludeTestLibraries { + args = append(args, excludeArgument, testSourcePattern) + logger.Info(logInfoExcludeTestLibraries) + } + logger.Info(fmt.Sprintf(logInfoCommandLine, strings.Join(args, " "))) transitiveValue := fmt.Sprintf("%v", s.transitiveLibraries) diff --git a/features/scans/syft/service_test.go b/features/scans/syft/service_test.go index 701a6d2..02d3dc7 100644 --- a/features/scans/syft/service_test.go +++ b/features/scans/syft/service_test.go @@ -12,7 +12,7 @@ import ( ) func TestNewSyftService(t *testing.T) { - service := newSyftService("./test", false, nil) + service := newSyftService("./test", false, false, nil) _, ok := service.(interfaces.ScanServiceImpl) assert.NotNil(t, service) @@ -24,7 +24,7 @@ func TestSyftStart(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "")) environment_variable.ReloadEnv() - service := newSyftService("./doesnotexist", false, nil) + service := newSyftService("./doesnotexist", false, false, nil) ok, err := service.Start() @@ -37,7 +37,7 @@ func TestSyftStart(t *testing.T) { _ = os.Setenv("SCAN_DIR", os.TempDir()) environment_variable.ReloadEnv() - svc := newSyftService(".", false, nil).(*SyftServiceImpl) + svc := newSyftService(".", false, false, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, _ []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { return false, fmt.Errorf("runner error") } @@ -52,7 +52,7 @@ func TestSyftStart(t *testing.T) { _ = os.Setenv("SCAN_DIR", os.TempDir()) environment_variable.ReloadEnv() - svc := newSyftService(".", true, nil).(*SyftServiceImpl) + svc := newSyftService(".", true, false, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, _ []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { return true, nil } @@ -62,11 +62,49 @@ func TestSyftStart(t *testing.T) { assert.Nil(t, err) assert.True(t, ok) }) + + t.Run("Should pass exclude test libraries arg when excludeTestLibraries is true", func(t *testing.T) { + _ = os.Setenv("SCAN_DIR", os.TempDir()) + environment_variable.ReloadEnv() + + var capturedArgs []string + svc := newSyftService(".", false, true, nil).(*SyftServiceImpl) + svc.runner = func(_ string, _ string, args []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { + capturedArgs = args + return true, nil + } + + ok, err := svc.Start() + + assert.Nil(t, err) + assert.True(t, ok) + assert.Contains(t, capturedArgs, excludeArgument) + assert.Contains(t, capturedArgs, testSourcePattern) + }) + + t.Run("Should not pass exclude test libraries arg when excludeTestLibraries is false", func(t *testing.T) { + _ = os.Setenv("SCAN_DIR", os.TempDir()) + environment_variable.ReloadEnv() + + var capturedArgs []string + svc := newSyftService(".", false, false, nil).(*SyftServiceImpl) + svc.runner = func(_ string, _ string, args []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { + capturedArgs = args + return true, nil + } + + ok, err := svc.Start() + + assert.Nil(t, err) + assert.True(t, ok) + assert.NotContains(t, capturedArgs, excludeArgument) + assert.NotContains(t, capturedArgs, testSourcePattern) + }) } func TestSyftLoadFindings(t *testing.T) { t.Run("Should return nil findings and nil error", func(t *testing.T) { - service := newSyftService("./test", false, nil) + service := newSyftService("./test", false, false, nil) findings, err := service.LoadFindings() @@ -77,7 +115,7 @@ func TestSyftLoadFindings(t *testing.T) { func TestSyftSync(t *testing.T) { t.Run("Should return nil error", func(t *testing.T) { - service := newSyftService("./test", false, nil) + service := newSyftService("./test", false, false, nil) err := service.Sync(1, "main", nil) diff --git a/loader/dto.go b/loader/dto.go index fa0d3db..d2c41e1 100644 --- a/loader/dto.go +++ b/loader/dto.go @@ -27,6 +27,14 @@ type ( // dependencies from Maven Central during SBOM generation. Disabled by // default because network resolution significantly increases scan time. TransitiveLibraries bool `toml:"transitive_libraries"` + // ExcludeTestLibraries controls whether Syft skips test source directories + // (e.g. **/src/test/**) during SBOM generation. Enabling this reduces noise + // from test-only code paths. Note: test-scoped Maven dependencies declared + // in pom.xml are not affected because Syft has no native scope filter for + // Java; this option only excludes paths from the file-system scan. For + // JavaScript projects, dev dependencies are already excluded unconditionally + // via the include-dev-dependencies: false setting in syft.yaml. + ExcludeTestLibraries bool `toml:"exclude_test_libraries"` } // Opengrep contains the configuration for the OpenGrep SAST scanner. From 8eb64bdf569ec42a56cf1fc934e4cfc17c757f1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:33:53 +0000 Subject: [PATCH 02/17] refactor: replace ExcludeTestLibraries bool with SyftExclude []string pattern Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/e01e7329-f9df-4f43-a4fc-0d93accf8d8f Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- config.toml | 2 +- features/scans/syft/const.go | 11 +++------- features/scans/syft/factory.go | 10 +++++----- features/scans/syft/factory_test.go | 13 ++++++------ features/scans/syft/service.go | 31 ++++++++++++++--------------- features/scans/syft/service_test.go | 26 +++++++++++++----------- loader/dto.go | 17 ++++++++-------- 7 files changed, 54 insertions(+), 56 deletions(-) diff --git a/config.toml b/config.toml index 44e475d..9020eaf 100644 --- a/config.toml +++ b/config.toml @@ -9,7 +9,7 @@ platform = "Dockerfile" [grype] ignore_states = "not-fixed,unknown,wont-fix" transitive_libraries = false -# exclude_test_libraries = false # set to true to skip test source directories (e.g. src/test/) from the SBOM scan +# syft_exclude = ["**/src/test/**"] # glob patterns passed to Syft --exclude to skip paths during SBOM generation [opengrep] exclude = ["tests/**"] diff --git a/features/scans/syft/const.go b/features/scans/syft/const.go index 0b17d0b..9f4259a 100644 --- a/features/scans/syft/const.go +++ b/features/scans/syft/const.go @@ -14,10 +14,6 @@ const ( excludeArgument = "--exclude" ) -const ( - testSourcePattern = "**/src/test/**" -) - const ( outputFolder = "results" syftJsonOutputNameParameter = "sbom.syft.json" @@ -25,10 +21,9 @@ const ( ) const ( - logInfoCommandLine = "Command line invoked [%s]" - logInfoTransitiveLibraries = "Transitive libraries resolution is enabled. This may significantly increase scan time." - logInfoExcludeTestLibraries = "Test source directory exclusion is enabled. Paths matching **/src/test/** will be skipped." - logErrorDirectoryNotFound = "Cannot find directory [%s]" + logInfoCommandLine = "Command line invoked [%s]" + logInfoTransitiveLibraries = "Transitive libraries resolution is enabled. This may significantly increase scan time." + logErrorDirectoryNotFound = "Cannot find directory [%s]" ) const ( diff --git a/features/scans/syft/factory.go b/features/scans/syft/factory.go index cbfc85a..3618512 100644 --- a/features/scans/syft/factory.go +++ b/features/scans/syft/factory.go @@ -7,15 +7,15 @@ import ( // GetSyftService constructs and returns a ScanServiceImpl for the Syft SBOM generator // using the provided loader configuration. When a Grype configuration is present, -// its TransitiveLibraries and ExcludeTestLibraries flags are forwarded to the Syft +// its TransitiveLibraries flag and SyftExclude patterns are forwarded to the Syft // service to control whether transitive Java dependencies are resolved from Maven -// Central and whether test source directories are excluded during SBOM generation. +// Central and which filesystem paths are excluded during SBOM generation. func GetSyftService(config loader.Config) interfaces.ScanServiceImpl { transitiveLibraries := false - excludeTestLibraries := false + var exclude []string if config.Grype != nil { transitiveLibraries = config.Grype.TransitiveLibraries - excludeTestLibraries = config.Grype.ExcludeTestLibraries + exclude = config.Grype.SyftExclude } - return newSyftService(config.Path, transitiveLibraries, excludeTestLibraries, config.Proxy.ToEnv()) + return newSyftService(config.Path, transitiveLibraries, exclude, config.Proxy.ToEnv()) } diff --git a/features/scans/syft/factory_test.go b/features/scans/syft/factory_test.go index 4e5da75..e628599 100644 --- a/features/scans/syft/factory_test.go +++ b/features/scans/syft/factory_test.go @@ -33,21 +33,22 @@ func TestGetSyftService(t *testing.T) { assert.True(t, ok) }) - t.Run("Should use excludeTestLibraries from grype config when false", func(t *testing.T) { - service := GetSyftService(loader.Config{Grype: &loader.Grype{ExcludeTestLibraries: false}}) + t.Run("Should use syft_exclude from grype config when empty", func(t *testing.T) { + service := GetSyftService(loader.Config{Grype: &loader.Grype{SyftExclude: nil}}) svc, ok := service.(*SyftServiceImpl) assert.NotNil(t, service) assert.True(t, ok) - assert.False(t, svc.excludeTestLibraries) + assert.Empty(t, svc.exclude) }) - t.Run("Should use excludeTestLibraries from grype config when true", func(t *testing.T) { - service := GetSyftService(loader.Config{Grype: &loader.Grype{ExcludeTestLibraries: true}}) + t.Run("Should use syft_exclude from grype config when set", func(t *testing.T) { + patterns := []string{"**/src/test/**", "**/testdata/**"} + service := GetSyftService(loader.Config{Grype: &loader.Grype{SyftExclude: patterns}}) svc, ok := service.(*SyftServiceImpl) assert.NotNil(t, service) assert.True(t, ok) - assert.True(t, svc.excludeTestLibraries) + assert.Equal(t, patterns, svc.exclude) }) } diff --git a/features/scans/syft/service.go b/features/scans/syft/service.go index 2d0bddc..673a0eb 100644 --- a/features/scans/syft/service.go +++ b/features/scans/syft/service.go @@ -20,27 +20,27 @@ type execRunner func(binaryPath string, dirPath string, args []string, stdout io // SyftServiceImpl implements ScanServiceImpl for the Syft SBOM generator. type SyftServiceImpl struct { - path string - transitiveLibraries bool - excludeTestLibraries bool - proxyEnv []string - runner execRunner + path string + transitiveLibraries bool + exclude []string + proxyEnv []string + runner execRunner } // newSyftService builds a SyftServiceImpl from the scan path, resolving it // relative to the SCAN_DIR environment variable. transitiveLibraries controls // whether Syft resolves transitive Java dependencies from Maven Central. -// excludeTestLibraries controls whether test source directories are excluded -// from the Syft filesystem scan (e.g. **/src/test/**). +// exclude is a list of glob patterns passed to Syft via --exclude to skip +// matching paths during SBOM generation (e.g. ["**/src/test/**"]). // proxyEnv is an optional list of "KEY=VALUE" proxy environment variable entries // (see loader.Proxy.ToEnv) forwarded to the Syft process. -func newSyftService(path string, transitiveLibraries bool, excludeTestLibraries bool, proxyEnv []string) interfaces.ScanServiceImpl { +func newSyftService(path string, transitiveLibraries bool, exclude []string, proxyEnv []string) interfaces.ScanServiceImpl { return &SyftServiceImpl{ - path: fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], path), - transitiveLibraries: transitiveLibraries, - excludeTestLibraries: excludeTestLibraries, - proxyEnv: proxyEnv, - runner: exec.Wrap, + path: fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], path), + transitiveLibraries: transitiveLibraries, + exclude: exclude, + proxyEnv: proxyEnv, + runner: exec.Wrap, } } @@ -65,9 +65,8 @@ func (s *SyftServiceImpl) Start() (bool, error) { logger.Info(logInfoTransitiveLibraries) } - if s.excludeTestLibraries { - args = append(args, excludeArgument, testSourcePattern) - logger.Info(logInfoExcludeTestLibraries) + for _, pattern := range s.exclude { + args = append(args, excludeArgument, pattern) } logger.Info(fmt.Sprintf(logInfoCommandLine, strings.Join(args, " "))) diff --git a/features/scans/syft/service_test.go b/features/scans/syft/service_test.go index 02d3dc7..cbe5b51 100644 --- a/features/scans/syft/service_test.go +++ b/features/scans/syft/service_test.go @@ -12,7 +12,7 @@ import ( ) func TestNewSyftService(t *testing.T) { - service := newSyftService("./test", false, false, nil) + service := newSyftService("./test", false, nil, nil) _, ok := service.(interfaces.ScanServiceImpl) assert.NotNil(t, service) @@ -24,7 +24,7 @@ func TestSyftStart(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "")) environment_variable.ReloadEnv() - service := newSyftService("./doesnotexist", false, false, nil) + service := newSyftService("./doesnotexist", false, nil, nil) ok, err := service.Start() @@ -37,7 +37,7 @@ func TestSyftStart(t *testing.T) { _ = os.Setenv("SCAN_DIR", os.TempDir()) environment_variable.ReloadEnv() - svc := newSyftService(".", false, false, nil).(*SyftServiceImpl) + svc := newSyftService(".", false, nil, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, _ []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { return false, fmt.Errorf("runner error") } @@ -52,7 +52,7 @@ func TestSyftStart(t *testing.T) { _ = os.Setenv("SCAN_DIR", os.TempDir()) environment_variable.ReloadEnv() - svc := newSyftService(".", true, false, nil).(*SyftServiceImpl) + svc := newSyftService(".", true, nil, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, _ []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { return true, nil } @@ -63,12 +63,13 @@ func TestSyftStart(t *testing.T) { assert.True(t, ok) }) - t.Run("Should pass exclude test libraries arg when excludeTestLibraries is true", func(t *testing.T) { + t.Run("Should pass each exclude pattern as a separate --exclude arg", func(t *testing.T) { _ = os.Setenv("SCAN_DIR", os.TempDir()) environment_variable.ReloadEnv() + patterns := []string{"**/src/test/**", "**/testdata/**"} var capturedArgs []string - svc := newSyftService(".", false, true, nil).(*SyftServiceImpl) + svc := newSyftService(".", false, patterns, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, args []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { capturedArgs = args return true, nil @@ -79,15 +80,17 @@ func TestSyftStart(t *testing.T) { assert.Nil(t, err) assert.True(t, ok) assert.Contains(t, capturedArgs, excludeArgument) - assert.Contains(t, capturedArgs, testSourcePattern) + for _, p := range patterns { + assert.Contains(t, capturedArgs, p) + } }) - t.Run("Should not pass exclude test libraries arg when excludeTestLibraries is false", func(t *testing.T) { + t.Run("Should not pass --exclude arg when exclude list is empty", func(t *testing.T) { _ = os.Setenv("SCAN_DIR", os.TempDir()) environment_variable.ReloadEnv() var capturedArgs []string - svc := newSyftService(".", false, false, nil).(*SyftServiceImpl) + svc := newSyftService(".", false, nil, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, args []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { capturedArgs = args return true, nil @@ -98,13 +101,12 @@ func TestSyftStart(t *testing.T) { assert.Nil(t, err) assert.True(t, ok) assert.NotContains(t, capturedArgs, excludeArgument) - assert.NotContains(t, capturedArgs, testSourcePattern) }) } func TestSyftLoadFindings(t *testing.T) { t.Run("Should return nil findings and nil error", func(t *testing.T) { - service := newSyftService("./test", false, false, nil) + service := newSyftService("./test", false, nil, nil) findings, err := service.LoadFindings() @@ -115,7 +117,7 @@ func TestSyftLoadFindings(t *testing.T) { func TestSyftSync(t *testing.T) { t.Run("Should return nil error", func(t *testing.T) { - service := newSyftService("./test", false, false, nil) + service := newSyftService("./test", false, nil, nil) err := service.Sync(1, "main", nil) diff --git a/loader/dto.go b/loader/dto.go index d2c41e1..aa38304 100644 --- a/loader/dto.go +++ b/loader/dto.go @@ -27,14 +27,15 @@ type ( // dependencies from Maven Central during SBOM generation. Disabled by // default because network resolution significantly increases scan time. TransitiveLibraries bool `toml:"transitive_libraries"` - // ExcludeTestLibraries controls whether Syft skips test source directories - // (e.g. **/src/test/**) during SBOM generation. Enabling this reduces noise - // from test-only code paths. Note: test-scoped Maven dependencies declared - // in pom.xml are not affected because Syft has no native scope filter for - // Java; this option only excludes paths from the file-system scan. For - // JavaScript projects, dev dependencies are already excluded unconditionally - // via the include-dev-dependencies: false setting in syft.yaml. - ExcludeTestLibraries bool `toml:"exclude_test_libraries"` + // SyftExclude is a list of glob patterns passed to Syft via --exclude to + // skip matching paths during SBOM generation. Use this to reduce noise from + // test source directories or other paths that should not appear in the SBOM + // (e.g. ["**/src/test/**"]). Note: test-scoped Maven dependencies declared + // in pom.xml are not affected because Syft has no native Maven scope filter; + // this option only excludes paths from the filesystem scan. For JavaScript + // projects, dev dependencies are already excluded unconditionally via the + // include-dev-dependencies: false setting in syft.yaml. + SyftExclude []string `toml:"syft_exclude"` } // Opengrep contains the configuration for the OpenGrep SAST scanner. From 653df33c2be8abb615d28c5c2222e1e6d03e93e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:37:21 +0000 Subject: [PATCH 03/17] docs: update README for syft_exclude array replacing exclude_test_libraries bool Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/40bf421f-48fb-4e1a-af6b-6d04a9a38bc7 Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 81040af..0633d1a 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,10 @@ ignore_states = "not-fixed,unknown,wont-fix" transitive_libraries = false # Optional list of path patterns to exclude from Grype scanning. # exclude = ["**/vendor/**", "**/testdata/**"] +# Optional list of glob patterns passed to Syft via --exclude during SBOM generation. +# Use this to skip paths such as test sources from the SBOM (e.g. src/test/). +# Note: test-scoped pom.xml dependencies are not affected (Syft has no Maven scope filter). +# syft_exclude = ["**/src/test/**"] # OpenGrep – static application security testing (SAST) scanner. [opengrep] @@ -182,6 +186,7 @@ transitive_libraries = false | `[grype].ignore_states` | string | no | Comma-separated Grype vulnerability states to suppress (e.g. `not-fixed,unknown,wont-fix`). | | `[grype].transitive_libraries` | bool | no | When `true`, Syft resolves transitive Java dependencies via Maven Central. Default: `false`. | | `[grype].exclude` | string array | no | Path glob patterns to exclude from Grype scanning (e.g. `["**/vendor/**"]`). | +| `[grype].syft_exclude` | string array | no | Path glob patterns passed to Syft via `--exclude` during SBOM generation (e.g. `["**/src/test/**"]`). Excludes filesystem paths only; test-scoped `pom.xml` dependencies are unaffected. | | `[opengrep].exclude` | string array | no | Path glob patterns to exclude from OpenGrep scanning (e.g. `["**/vendor/**"]`). | | `[opengrep].exclude_rule` | string array | no | OpenGrep rule IDs to skip (e.g. `["python.lang.security.audit.formatted-sql-query.formatted-sql-query"]`). | | `[proxy].http_proxy` | string | no | HTTP proxy URL forwarded as `HTTP_PROXY` / `http_proxy` to all scanner sub-processes. | From 49b2a9bc311ded480b49d1e644a7acd7e6840abb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:47:57 +0000 Subject: [PATCH 04/17] feat(grype): populate CWE/CVE field with CVE ID; rename column header to CWE/CVE Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/4e7ec1fa-5cb1-4759-b880-d9bc99e47e52 Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- README.md | 2 ++ display/display.go | 2 +- display/display_test.go | 2 +- features/scans/grype/service.go | 1 + features/scans/grype/service_test.go | 2 ++ 5 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0633d1a..190ef69 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,8 @@ The Grype scanner uploads its JSON output file to DefectDojo as a `multipart/for | Do not reactivate | `true` | Previously suppressed findings are not reactivated on reimport | | Branch tag | `` | Associates the results with the scanned branch | +The **CWE/CVE** column in the CLI output and in file exports is populated with the CVE identifier (e.g. `CVE-2021-44228`) reported by Grype for each vulnerability match. KICS and OpenGrep findings use a CWE number in the same column. + ### OpenGrep Sync Behaviour The OpenGrep scanner uploads its JSON output file to DefectDojo as a `multipart/form-data` request. Before uploading, the file is enriched so that each result contains an `extra.severity` field required by DefectDojo's Semgrep JSON Report parser (the value is copied from `extra.metadata.impact`). The endpoint used is `/api/v2/import-scan/` on the first run and `/api/v2/reimport-scan/` on subsequent runs. The following options are set on every upload: diff --git a/display/display.go b/display/display.go index ead953d..e9a9eec 100644 --- a/display/display.go +++ b/display/display.go @@ -18,7 +18,7 @@ const ( rowEngine = "Engine" rowSeverity = "Severity" rowName = "Name" - rowCwe = "CWE" + rowCwe = "CWE/CVE" rowDescription = "Description" rowSinkFile = "Sink File" rowSinkLine = "Sink Line" diff --git a/display/display_test.go b/display/display_test.go index d4966fc..7183723 100644 --- a/display/display_test.go +++ b/display/display_test.go @@ -138,7 +138,7 @@ func TestDumpFindings_CSV_ContainsHeaders(t *testing.T) { assert.Nil(t, err) output := buf.String() - assert.True(t, strings.HasPrefix(output, "Engine,Severity,Name,CWE,Description,SinkFile,SinkLine,Recommendation,Status")) + assert.True(t, strings.HasPrefix(output, "Engine,Severity,Name,CWE/CVE,Description,SinkFile,SinkLine,Recommendation,Status")) } func TestDumpFindings_CSV_ContainsStatus(t *testing.T) { diff --git a/features/scans/grype/service.go b/features/scans/grype/service.go index 9499675..4fbd1c3 100644 --- a/features/scans/grype/service.go +++ b/features/scans/grype/service.go @@ -107,6 +107,7 @@ func (s *GrypeServiceImpl) LoadFindings() ([]models.Finding, error) { Severity: severity, Name: fmt.Sprintf("%s %s", match.Artifact.Name, match.Artifact.Version), VulnId: match.Vulnerability.ID, + Cwe: match.Vulnerability.ID, Description: match.Vulnerability.Description, SinkFile: sinkFile, Recommendation: recommendation, diff --git a/features/scans/grype/service_test.go b/features/scans/grype/service_test.go index 74d276d..6df3aca 100644 --- a/features/scans/grype/service_test.go +++ b/features/scans/grype/service_test.go @@ -104,11 +104,13 @@ func TestLoadFindings(t *testing.T) { assert.EqualValues(t, "test-package 1.0.0", findings[0].Name) assert.EqualValues(t, "CVE-2021-1234", findings[0].VulnId) + assert.EqualValues(t, "CVE-2021-1234", findings[0].Cwe) assert.EqualValues(t, "HIGH", findings[0].Severity) assert.EqualValues(t, "Upgrade to 1.2.0", findings[0].Recommendation) assert.EqualValues(t, "another-package 2.0.0", findings[1].Name) assert.EqualValues(t, "CVE-2021-5678", findings[1].VulnId) + assert.EqualValues(t, "CVE-2021-5678", findings[1].Cwe) assert.EqualValues(t, "MEDIUM", findings[1].Severity) assert.EqualValues(t, "", findings[1].Recommendation) }) From 72ae6d06ec832c14bbb24644714cfd8e41b3bc81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:54:44 +0000 Subject: [PATCH 05/17] feat(syft): add syft_max_parent_recursive_depth toml parameter Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/b6b4ba0c-f694-43a4-b670-c8bc709189e0 Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- README.md | 4 +++ features/scans/syft/const.go | 1 + features/scans/syft/factory.go | 11 +++--- features/scans/syft/factory_test.go | 27 +++++++++++++++ features/scans/syft/service.go | 28 ++++++++++------ features/scans/syft/service_test.go | 52 ++++++++++++++++++++++++----- loader/dto.go | 5 +++ loader/loader_test.go | 1 + loader/mocks/config_with_grype.toml | 1 + 9 files changed, 107 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 190ef69..aaa1a59 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,9 @@ transitive_libraries = false # Use this to skip paths such as test sources from the SBOM (e.g. src/test/). # Note: test-scoped pom.xml dependencies are not affected (Syft has no Maven scope filter). # syft_exclude = ["**/src/test/**"] +# Depth to recursively resolve parent POMs (env: SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH). +# 0 means no limit (the default). +# syft_max_parent_recursive_depth = 0 # OpenGrep – static application security testing (SAST) scanner. [opengrep] @@ -187,6 +190,7 @@ transitive_libraries = false | `[grype].transitive_libraries` | bool | no | When `true`, Syft resolves transitive Java dependencies via Maven Central. Default: `false`. | | `[grype].exclude` | string array | no | Path glob patterns to exclude from Grype scanning (e.g. `["**/vendor/**"]`). | | `[grype].syft_exclude` | string array | no | Path glob patterns passed to Syft via `--exclude` during SBOM generation (e.g. `["**/src/test/**"]`). Excludes filesystem paths only; test-scoped `pom.xml` dependencies are unaffected. | +| `[grype].syft_max_parent_recursive_depth` | int | no | Maximum number of parent POM levels Syft will recursively resolve during Java/Maven analysis. `0` (default) means no limit. Forwarded as `SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH`. | | `[opengrep].exclude` | string array | no | Path glob patterns to exclude from OpenGrep scanning (e.g. `["**/vendor/**"]`). | | `[opengrep].exclude_rule` | string array | no | OpenGrep rule IDs to skip (e.g. `["python.lang.security.audit.formatted-sql-query.formatted-sql-query"]`). | | `[proxy].http_proxy` | string | no | HTTP proxy URL forwarded as `HTTP_PROXY` / `http_proxy` to all scanner sub-processes. | diff --git a/features/scans/syft/const.go b/features/scans/syft/const.go index 9f4259a..a87eb95 100644 --- a/features/scans/syft/const.go +++ b/features/scans/syft/const.go @@ -33,4 +33,5 @@ const ( const ( envJavaUseNetwork = "SYFT_JAVA_USE_NETWORK" envJavaResolveTransitiveDependencies = "SYFT_JAVA_RESOLVE_TRANSITIVE_DEPENDENCIES" + envJavaMaxParentRecursiveDepth = "SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH" ) diff --git a/features/scans/syft/factory.go b/features/scans/syft/factory.go index 3618512..0c1359b 100644 --- a/features/scans/syft/factory.go +++ b/features/scans/syft/factory.go @@ -7,15 +7,18 @@ import ( // GetSyftService constructs and returns a ScanServiceImpl for the Syft SBOM generator // using the provided loader configuration. When a Grype configuration is present, -// its TransitiveLibraries flag and SyftExclude patterns are forwarded to the Syft -// service to control whether transitive Java dependencies are resolved from Maven -// Central and which filesystem paths are excluded during SBOM generation. +// its TransitiveLibraries flag, SyftExclude patterns, and SyftMaxParentRecursiveDepth +// are forwarded to the Syft service to control whether transitive Java dependencies +// are resolved from Maven Central, which filesystem paths are excluded during SBOM +// generation, and how many parent POM levels are recursively resolved. func GetSyftService(config loader.Config) interfaces.ScanServiceImpl { transitiveLibraries := false var exclude []string + maxParentRecursiveDepth := 0 if config.Grype != nil { transitiveLibraries = config.Grype.TransitiveLibraries exclude = config.Grype.SyftExclude + maxParentRecursiveDepth = config.Grype.SyftMaxParentRecursiveDepth } - return newSyftService(config.Path, transitiveLibraries, exclude, config.Proxy.ToEnv()) + return newSyftService(config.Path, transitiveLibraries, exclude, maxParentRecursiveDepth, config.Proxy.ToEnv()) } diff --git a/features/scans/syft/factory_test.go b/features/scans/syft/factory_test.go index e628599..bc98b2f 100644 --- a/features/scans/syft/factory_test.go +++ b/features/scans/syft/factory_test.go @@ -51,4 +51,31 @@ func TestGetSyftService(t *testing.T) { assert.True(t, ok) assert.Equal(t, patterns, svc.exclude) }) + + t.Run("Should use syft_max_parent_recursive_depth from grype config when zero", func(t *testing.T) { + service := GetSyftService(loader.Config{Grype: &loader.Grype{SyftMaxParentRecursiveDepth: 0}}) + svc, ok := service.(*SyftServiceImpl) + + assert.NotNil(t, service) + assert.True(t, ok) + assert.Equal(t, 0, svc.maxParentRecursiveDepth) + }) + + t.Run("Should use syft_max_parent_recursive_depth from grype config when set", func(t *testing.T) { + service := GetSyftService(loader.Config{Grype: &loader.Grype{SyftMaxParentRecursiveDepth: 5}}) + svc, ok := service.(*SyftServiceImpl) + + assert.NotNil(t, service) + assert.True(t, ok) + assert.Equal(t, 5, svc.maxParentRecursiveDepth) + }) + + t.Run("Should default syft_max_parent_recursive_depth to 0 when grype config is nil", func(t *testing.T) { + service := GetSyftService(loader.Config{}) + svc, ok := service.(*SyftServiceImpl) + + assert.NotNil(t, service) + assert.True(t, ok) + assert.Equal(t, 0, svc.maxParentRecursiveDepth) + }) } diff --git a/features/scans/syft/service.go b/features/scans/syft/service.go index 673a0eb..9cfb938 100644 --- a/features/scans/syft/service.go +++ b/features/scans/syft/service.go @@ -20,11 +20,12 @@ type execRunner func(binaryPath string, dirPath string, args []string, stdout io // SyftServiceImpl implements ScanServiceImpl for the Syft SBOM generator. type SyftServiceImpl struct { - path string - transitiveLibraries bool - exclude []string - proxyEnv []string - runner execRunner + path string + transitiveLibraries bool + exclude []string + maxParentRecursiveDepth int + proxyEnv []string + runner execRunner } // newSyftService builds a SyftServiceImpl from the scan path, resolving it @@ -32,15 +33,19 @@ type SyftServiceImpl struct { // whether Syft resolves transitive Java dependencies from Maven Central. // exclude is a list of glob patterns passed to Syft via --exclude to skip // matching paths during SBOM generation (e.g. ["**/src/test/**"]). +// maxParentRecursiveDepth sets how many parent POM levels Syft will resolve; +// 0 means no limit (the default). It is forwarded as the +// SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH environment variable. // proxyEnv is an optional list of "KEY=VALUE" proxy environment variable entries // (see loader.Proxy.ToEnv) forwarded to the Syft process. -func newSyftService(path string, transitiveLibraries bool, exclude []string, proxyEnv []string) interfaces.ScanServiceImpl { +func newSyftService(path string, transitiveLibraries bool, exclude []string, maxParentRecursiveDepth int, proxyEnv []string) interfaces.ScanServiceImpl { return &SyftServiceImpl{ - path: fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], path), - transitiveLibraries: transitiveLibraries, - exclude: exclude, - proxyEnv: proxyEnv, - runner: exec.Wrap, + path: fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], path), + transitiveLibraries: transitiveLibraries, + exclude: exclude, + maxParentRecursiveDepth: maxParentRecursiveDepth, + proxyEnv: proxyEnv, + runner: exec.Wrap, } } @@ -75,6 +80,7 @@ func (s *SyftServiceImpl) Start() (bool, error) { extraEnv := []string{ fmt.Sprintf("%s=%s", envJavaUseNetwork, transitiveValue), fmt.Sprintf("%s=%s", envJavaResolveTransitiveDependencies, transitiveValue), + fmt.Sprintf("%s=%d", envJavaMaxParentRecursiveDepth, s.maxParentRecursiveDepth), } extraEnv = append(extraEnv, s.proxyEnv...) return s.runner(binaryPath, dirPath, args, os.Stdout, os.Stderr, extraEnv...) diff --git a/features/scans/syft/service_test.go b/features/scans/syft/service_test.go index cbe5b51..ae948d7 100644 --- a/features/scans/syft/service_test.go +++ b/features/scans/syft/service_test.go @@ -12,7 +12,7 @@ import ( ) func TestNewSyftService(t *testing.T) { - service := newSyftService("./test", false, nil, nil) + service := newSyftService("./test", false, nil, 0, nil) _, ok := service.(interfaces.ScanServiceImpl) assert.NotNil(t, service) @@ -24,7 +24,7 @@ func TestSyftStart(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "")) environment_variable.ReloadEnv() - service := newSyftService("./doesnotexist", false, nil, nil) + service := newSyftService("./doesnotexist", false, nil, 0, nil) ok, err := service.Start() @@ -37,7 +37,7 @@ func TestSyftStart(t *testing.T) { _ = os.Setenv("SCAN_DIR", os.TempDir()) environment_variable.ReloadEnv() - svc := newSyftService(".", false, nil, nil).(*SyftServiceImpl) + svc := newSyftService(".", false, nil, 0, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, _ []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { return false, fmt.Errorf("runner error") } @@ -52,7 +52,7 @@ func TestSyftStart(t *testing.T) { _ = os.Setenv("SCAN_DIR", os.TempDir()) environment_variable.ReloadEnv() - svc := newSyftService(".", true, nil, nil).(*SyftServiceImpl) + svc := newSyftService(".", true, nil, 0, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, _ []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { return true, nil } @@ -69,7 +69,7 @@ func TestSyftStart(t *testing.T) { patterns := []string{"**/src/test/**", "**/testdata/**"} var capturedArgs []string - svc := newSyftService(".", false, patterns, nil).(*SyftServiceImpl) + svc := newSyftService(".", false, patterns, 0, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, args []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { capturedArgs = args return true, nil @@ -90,7 +90,7 @@ func TestSyftStart(t *testing.T) { environment_variable.ReloadEnv() var capturedArgs []string - svc := newSyftService(".", false, nil, nil).(*SyftServiceImpl) + svc := newSyftService(".", false, nil, 0, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, args []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { capturedArgs = args return true, nil @@ -102,11 +102,47 @@ func TestSyftStart(t *testing.T) { assert.True(t, ok) assert.NotContains(t, capturedArgs, excludeArgument) }) + + t.Run("Should pass SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH env var with configured value", func(t *testing.T) { + _ = os.Setenv("SCAN_DIR", os.TempDir()) + environment_variable.ReloadEnv() + + var capturedEnv []string + svc := newSyftService(".", false, nil, 3, nil).(*SyftServiceImpl) + svc.runner = func(_ string, _ string, _ []string, _ io.Writer, _ io.Writer, extraEnv ...string) (bool, error) { + capturedEnv = extraEnv + return true, nil + } + + ok, err := svc.Start() + + assert.Nil(t, err) + assert.True(t, ok) + assert.Contains(t, capturedEnv, "SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH=3") + }) + + t.Run("Should pass SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH=0 when default value is used", func(t *testing.T) { + _ = os.Setenv("SCAN_DIR", os.TempDir()) + environment_variable.ReloadEnv() + + var capturedEnv []string + svc := newSyftService(".", false, nil, 0, nil).(*SyftServiceImpl) + svc.runner = func(_ string, _ string, _ []string, _ io.Writer, _ io.Writer, extraEnv ...string) (bool, error) { + capturedEnv = extraEnv + return true, nil + } + + ok, err := svc.Start() + + assert.Nil(t, err) + assert.True(t, ok) + assert.Contains(t, capturedEnv, "SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH=0") + }) } func TestSyftLoadFindings(t *testing.T) { t.Run("Should return nil findings and nil error", func(t *testing.T) { - service := newSyftService("./test", false, nil, nil) + service := newSyftService("./test", false, nil, 0, nil) findings, err := service.LoadFindings() @@ -117,7 +153,7 @@ func TestSyftLoadFindings(t *testing.T) { func TestSyftSync(t *testing.T) { t.Run("Should return nil error", func(t *testing.T) { - service := newSyftService("./test", false, nil, nil) + service := newSyftService("./test", false, nil, 0, nil) err := service.Sync(1, "main", nil) diff --git a/loader/dto.go b/loader/dto.go index aa38304..9587f17 100644 --- a/loader/dto.go +++ b/loader/dto.go @@ -36,6 +36,11 @@ type ( // projects, dev dependencies are already excluded unconditionally via the // include-dev-dependencies: false setting in syft.yaml. SyftExclude []string `toml:"syft_exclude"` + // SyftMaxParentRecursiveDepth controls how many levels of parent POMs Syft + // will recursively resolve during Java/Maven analysis. A value of 0 (the + // default) means no limit. Passed to Syft as the + // SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH environment variable. + SyftMaxParentRecursiveDepth int `toml:"syft_max_parent_recursive_depth"` } // Opengrep contains the configuration for the OpenGrep SAST scanner. diff --git a/loader/loader_test.go b/loader/loader_test.go index 6c2d95d..c20f7d9 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -28,6 +28,7 @@ func TestLoad(t *testing.T) { assert.Nil(t, config.Kics) assert.EqualValues(t, "not-fixed,unknown,wont-fix", config.Grype.IgnoreStates) assert.EqualValues(t, []string{"/etc/**"}, config.Grype.Exclude) + assert.EqualValues(t, 5, config.Grype.SyftMaxParentRecursiveDepth) }) t.Run("Should load configuration file without engine", func(t *testing.T) { diff --git a/loader/mocks/config_with_grype.toml b/loader/mocks/config_with_grype.toml index 62f7dff..cc953f1 100644 --- a/loader/mocks/config_with_grype.toml +++ b/loader/mocks/config_with_grype.toml @@ -4,3 +4,4 @@ path = "./" [grype] ignore_states = "not-fixed,unknown,wont-fix" exclude = ["/etc/**"] +syft_max_parent_recursive_depth = 5 From 3366e4f5b510bd4b0d703eb2ba9434b47e568b16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 09:07:32 +0000 Subject: [PATCH 06/17] Add CLAUDE.md with project context and development guidelines Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/ce548f51-4069-47df-ba92-157ec5e043bb Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- CLAUDE.md | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..daea336 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,167 @@ +# ScopeGuardian – Context for AI Assistants + +## Project Overview + +**ScopeGuardian** is a Go CLI tool that orchestrates security scanners (KICS, Grype/Syft, OpenGrep) against a codebase and optionally synchronises findings with [DefectDojo](https://github.com/DefectDojo/django-DefectDojo). It can enforce a security gate that exits with `-1` to block CI/CD pipelines when configured severity thresholds are exceeded. + +## Repository Layout + +``` +ScopeGuardian/ +├── main.go # Entry point: parse → load → scan → display → gate +├── config.toml # Sample configuration file +├── go.mod / go.sum # Go module (module name: ScopeGuardian, go 1.24.4) +├── Dockerfile # Multi-stage image (bundles KICS, Grype, Syft, OpenGrep) +├── docker-compose.yml # Local DefectDojo stack (PostgreSQL + Redis + Nginx) +├── .env.example # Template for environment variables +├── connectors/ +│ └── defectdojo/ # DefectDojo API client (import/reimport, findings, engagements) +│ └── client/ # HTTP client wrapper +├── display/ # Banner, findings table, JSON/CSV/raw dump +├── domains/ +│ ├── interfaces/ # ScanServiceImpl interface +│ └── models/ # Finding model, FindingStatus, FilterFindingsByStatus +├── engine/ +│ ├── engine.go # Two-phase parallel runner (prerequisites → dependents) +│ ├── engine_test.go +│ └── const.go # Log message constants +├── environnement_variable/ # Loads SCAN_DIR, DD_URL, DD_ACCESS_TOKEN from env +├── exec/ # Shell command execution helpers +├── features/ +│ ├── scans/ +│ │ ├── kics/ # KICS IaC scanner integration +│ │ ├── grype/ # Grype SCA scanner integration +│ │ ├── opengrep/ # OpenGrep SAST scanner integration +│ │ └── syft/ # Syft SBOM generator (prerequisite for Grype) +│ ├── security-gate/ # Threshold evaluation logic +│ └── sync/ # DefectDojo engagement resolution and finding marking +├── loader/ # TOML config loader +├── logger/ # Structured logging (slog wrapper + null logger) +└── parser/ # CLI flag parser (--projectName, --branch, --sync, etc.) +``` + +## Common Commands + +```bash +# Build +go build -o ScopeGuardian . + +# Run all tests +go test ./... + +# Run tests with verbose output +go test -v ./... + +# Run a basic scan (no sync, no gate) +SCAN_DIR=/path/to/repos ./ScopeGuardian --projectName my-service --branch main ./config.toml + +# Run a scan and write findings to JSON +SCAN_DIR=/path/to/repos ./ScopeGuardian --projectName my-service --branch main -o /tmp/findings.json ./config.toml + +# Run a scan, sync to DefectDojo, enforce gate +SCAN_DIR=/path/to/repos DD_URL=http://localhost:8080 DD_ACCESS_TOKEN= \ + ./ScopeGuardian --projectName my-service --branch main --sync --threshold critical=1,high=5 ./config.toml + +# Build Docker image +docker build -t ScopeGuardian . +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `SCAN_DIR` | always | Base directory; scan paths and result files are resolved relative to this | +| `DD_URL` | with `--sync` | Base URL of the DefectDojo instance (e.g. `http://localhost:8080`) | +| `DD_ACCESS_TOKEN` | with `--sync` | DefectDojo API v2 token | + +Copy `.env.example` → `.env` for local development. + +## Key CLI Flags + +| Flag | Description | +|------|-------------| +| `--projectName` | Project/product name; must match a DefectDojo Product name when using `--sync` | +| `--branch` | Branch being scanned | +| `--sync` | Upload results to DefectDojo and fetch back statuses | +| `--threshold` | `severity=count[,...]` — security gate thresholds | +| `--filter` | Comma-separated statuses to display/write (`ACTIVE`, `INACTIVE`, `DUPLICATE`). Default: `ACTIVE` | +| `-q` | Quiet mode (suppress logs) | +| `-o` | Output file path for findings | +| `--format` | Output format: `json` (default), `csv`, `raw` | + +## Architecture Notes + +### Engine (two-phase parallel scan) +1. **Prerequisites** — Syft SBOM generation runs first; failure is recorded. +2. **Dependent/independent scanners** — Grype, KICS, OpenGrep run concurrently. Grype is skipped if Syft failed. + +### Scanner interface (`domains/interfaces/ScanServiceImpl`) +Every scanner implements: +- `Start() (bool, error)` — runs the scanner binary, writes output file to `$SCAN_DIR/results/` +- `LoadFindings() ([]models.Finding, error)` — parses the output file into `Finding` structs +- `Sync(engagementId int, branch string, ddService) error` — uploads results to DefectDojo + +### Finding model (`domains/models/Finding`) +Fields include: `Title`, `Severity`, `Description`, `FilePath`, `Status` (`ACTIVE`/`INACTIVE`/`DUPLICATE`). + +### DefectDojo sync behaviour +- **First run** → `POST /api/v2/import-scan/` +- **Subsequent runs** → `POST /api/v2/reimport-scan/` (closes old findings, preserves triage decisions) +- Engagement name pattern: `-` +- Protected branches → 1-year engagement end date; others → 1-week end date + +### Security gate +- Counts findings **≥ configured severity** (CRITICAL > HIGH > MEDIUM > LOW > INFO). +- When `--sync` is active the gate uses DefectDojo's deduplicated ACTIVE findings, not raw local output. +- Exit code `-1` on failure. + +## Configuration File (`config.toml`) + +```toml +title = "Scope-guardian configuration file" +protected_branches = ["main", "master"] +path = "./my-service" # Relative to SCAN_DIR + +[kics] +platform = "Dockerfile" # KICS --type filter + +[grype] +ignore_states = "not-fixed,unknown,wont-fix" +transitive_libraries = false # true = resolve transitive Java deps (slow) + +[opengrep] +exclude = ["tests/**"] +exclude_rule = [] + +# [proxy] +# http_proxy = "http://proxy.company.com:3128" +# https_proxy = "http://proxy.company.com:3128" +# no_proxy = "localhost,127.0.0.1" +# ssl_cert_file = "/path/to/ca.pem" +``` + +## Code Conventions + +- **Module path**: `ScopeGuardian` (no domain prefix) +- **Logging**: use `logger.Info`, `logger.Error`, `logger.Err(err)` from the `logger` package — never `fmt.Println` or `log.*` +- **Log constants**: define log message strings as `const` in the package (see `engine/const.go`) +- **Tests**: use `github.com/stretchr/testify` and `github.com/golang/mock` +- **Error handling**: log and continue (scanner errors do not abort the whole run) +- **No global state**: scanners are instantiated via `GetService(config)` factory functions +- **Proxy forwarding**: pass proxy env vars to all scanner sub-processes; set both uppercase and lowercase variants + +## External Tool Binary Paths (inside Docker / expected locations) + +| Tool | Path | +|------|------| +| KICS | `/opt/kics/bin/kics` | +| OpenGrep | `/opt/opengrep/bin/opengrep` | +| Syft | `/opt/syft/bin/syft` | +| Grype | `/opt/grype/bin/grype` | + +## Testing + +- Unit tests live alongside source files (`*_test.go`). +- Run `go test ./...` from the repo root. +- Mocks are generated with `github.com/golang/mock`. +- No integration test suite is bundled; integration testing requires a live DefectDojo instance. From 1274962dfbedd14ad1d29cee80a3d31603f3a424 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 09:11:42 +0000 Subject: [PATCH 07/17] Remove env copy hint line from CLAUDE.md Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/28a5302d-19e4-4394-a081-4cb0554f883e Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- CLAUDE.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index daea336..781ba70 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,8 +74,6 @@ docker build -t ScopeGuardian . | `DD_URL` | with `--sync` | Base URL of the DefectDojo instance (e.g. `http://localhost:8080`) | | `DD_ACCESS_TOKEN` | with `--sync` | DefectDojo API v2 token | -Copy `.env.example` → `.env` for local development. - ## Key CLI Flags | Flag | Description | From 3056df75fd9a1fdabe4ce992be62fa813968b6bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 09:15:32 +0000 Subject: [PATCH 08/17] Clarify testing coverage expectation in CLAUDE.md Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/e7575d62-ea3e-4b6c-8c33-57256444d94c Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index 781ba70..0144923 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -162,4 +162,5 @@ exclude_rule = [] - Unit tests live alongside source files (`*_test.go`). - Run `go test ./...` from the repo root. - Mocks are generated with `github.com/golang/mock`. +- New code must be covered by unit tests with significant coverage; aim for full branch and error-path coverage on all non-trivial logic. - No integration test suite is bundled; integration testing requires a live DefectDojo instance. From 6e08df914f6b30100e27261310c4239814b801c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 09:19:25 +0000 Subject: [PATCH 09/17] Add .github/copilot-instructions.md skill to enforce README.md updates Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/cca37c05-b1b6-458e-a18a-c0b8c336df2f Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- .github/copilot-instructions.md | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..c6327df --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,35 @@ +# Copilot Agent Instructions + +## README.md Synchronisation + +Whenever you make code changes that affect any of the following areas, you **must** review and update `README.md` before considering the task complete: + +### Triggers — update README.md when changing: + +| Area | Examples | +|------|---------| +| **CLI flags** | Adding, removing, or renaming flags in `parser/` | +| **Environment variables** | Adding, removing, or renaming vars in `environnement_variable/` | +| **Configuration file schema** | New or changed keys in `config.toml` / loader | +| **Scanner integration** | Adding a new scanner, changing binary paths or output formats | +| **Security gate logic** | Threshold evaluation rules, exit code behaviour | +| **DefectDojo sync behaviour** | Import/reimport flow, engagement naming, end-date logic | +| **Docker image** | New tools bundled, changed base image, multi-stage changes | +| **External tool paths** | Binary locations for KICS, Grype, Syft, OpenGrep | +| **Go module / build requirements** | Minimum Go version, new mandatory build steps | + +### What to update in README.md + +- **CLI Usage** table — keep flags, types, and descriptions accurate. +- **Environment Variables** table — keep variable names and descriptions accurate. +- **Configuration File** section — reflect any new/changed `config.toml` keys with examples. +- **Prerequisites** list — add or remove tool dependencies as needed. +- **Quick Start** examples — update commands if invocation syntax changes. +- **How-it-works sections** — update architecture prose if behaviour changes. + +### How to apply this rule + +1. After completing your code changes, re-read the sections of `README.md` listed above. +2. If any section is now inaccurate or incomplete, edit it to match the new behaviour. +3. Commit the `README.md` update in the same PR as the code change. +4. If none of the trigger areas were touched, README.md does not need to change — but note this explicitly in your PR description. From 8a0770c6c67e418d1dddb9b09bb60a7b29d31652 Mon Sep 17 00:00:00 2001 From: nitrax Date: Fri, 1 May 2026 11:28:10 +0200 Subject: [PATCH 10/17] Create claude skill Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- .../skills/update-readme/SKILL.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/copilot-instructions.md => .claude/skills/update-readme/SKILL.md (100%) diff --git a/.github/copilot-instructions.md b/.claude/skills/update-readme/SKILL.md similarity index 100% rename from .github/copilot-instructions.md rename to .claude/skills/update-readme/SKILL.md From 2113a173f7034e0f07649ea80fe409183617265b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 09:30:09 +0000 Subject: [PATCH 11/17] Rewrite .claude/skills/update-readme/SKILL.md as a proper Claude Code skill Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/bfc8ab1b-41e8-4821-b8d1-180c11ebfcaf Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- .claude/skills/update-readme/SKILL.md | 85 ++++++++++++++++----------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/.claude/skills/update-readme/SKILL.md b/.claude/skills/update-readme/SKILL.md index c6327df..6e9ead1 100644 --- a/.claude/skills/update-readme/SKILL.md +++ b/.claude/skills/update-readme/SKILL.md @@ -1,35 +1,50 @@ -# Copilot Agent Instructions - -## README.md Synchronisation - -Whenever you make code changes that affect any of the following areas, you **must** review and update `README.md` before considering the task complete: - -### Triggers — update README.md when changing: - -| Area | Examples | -|------|---------| -| **CLI flags** | Adding, removing, or renaming flags in `parser/` | -| **Environment variables** | Adding, removing, or renaming vars in `environnement_variable/` | -| **Configuration file schema** | New or changed keys in `config.toml` / loader | -| **Scanner integration** | Adding a new scanner, changing binary paths or output formats | -| **Security gate logic** | Threshold evaluation rules, exit code behaviour | -| **DefectDojo sync behaviour** | Import/reimport flow, engagement naming, end-date logic | -| **Docker image** | New tools bundled, changed base image, multi-stage changes | -| **External tool paths** | Binary locations for KICS, Grype, Syft, OpenGrep | -| **Go module / build requirements** | Minimum Go version, new mandatory build steps | - -### What to update in README.md - -- **CLI Usage** table — keep flags, types, and descriptions accurate. -- **Environment Variables** table — keep variable names and descriptions accurate. -- **Configuration File** section — reflect any new/changed `config.toml` keys with examples. -- **Prerequisites** list — add or remove tool dependencies as needed. -- **Quick Start** examples — update commands if invocation syntax changes. -- **How-it-works sections** — update architecture prose if behaviour changes. - -### How to apply this rule - -1. After completing your code changes, re-read the sections of `README.md` listed above. -2. If any section is now inaccurate or incomplete, edit it to match the new behaviour. -3. Commit the `README.md` update in the same PR as the code change. -4. If none of the trigger areas were touched, README.md does not need to change — but note this explicitly in your PR description. +--- +name: update-readme +description: > + Reviews recent code changes and updates README.md to keep it accurate. + Invoke this skill after completing code changes that touch CLI flags, + environment variables, configuration schema, scanner integrations, + security-gate logic, DefectDojo sync behaviour, Docker image, or build + requirements. +--- + +# Update README Skill + +## When to invoke + +Run this skill after any code change that touches one of the following areas: + +| Area | Key files / packages | +|------|----------------------| +| CLI flags | `parser/` | +| Environment variables | `environnement_variable/` | +| Configuration file schema | `loader/`, `config.toml` | +| Scanner integration | `features/scans/*/`, `engine/` | +| Security gate logic | `features/security-gate/` | +| DefectDojo sync behaviour | `features/sync/`, `connectors/defectdojo/` | +| Docker image / binary paths | `Dockerfile` | +| Go module / build requirements | `go.mod`, `main.go` | + +## Steps + +1. **Read the changed files** — use the Read tool on every file you modified to recall exactly what changed. + +2. **Check each README section** — open `README.md` and evaluate the sections below against your changes: + + | README section | What to verify | + |----------------|----------------| + | **Prerequisites** | Tool names, binary paths, and minimum Go version are accurate | + | **Quick Start** | Example commands still work with the current flag names and env vars | + | **CLI Usage** | Flag table is complete; names, types, and descriptions match `parser/` | + | **Configuration File (`config.toml`)** | Every documented key exists; new keys are documented with examples | + | **Environment Variables** | Table matches the vars loaded in `environnement_variable/` | + | **How Engagements Are Handled** | Engagement naming and end-date rules are correct | + | **How the Sync Feature Works** | Import vs reimport logic, flow description | + | **How the Security Gate Works** | Threshold evaluation, severity ordering, exit code behaviour | + | **Running with Docker** | Image name, tool paths, and `docker run` examples are current | + +3. **Edit README.md** — for each section that is now inaccurate or missing information, apply the minimum edit needed to make it accurate. Do not rewrite sections that are still correct. + +4. **Report what changed** — after editing, briefly summarise which README sections were updated and why (one line per section is enough). + +5. **No-op case** — if none of the trigger areas were touched, state explicitly: "README.md does not require updates for this change." From be6b0e062feb2d9715a1b09e283532c0dd462697 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 16:44:32 +0000 Subject: [PATCH 12/17] Add .claude/skills/go-fmt/SKILL.md for go fmt skill Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/4e2a7ac4-93a8-4f27-b226-8d54ea0ddba2 Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- .claude/skills/go-fmt/SKILL.md | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .claude/skills/go-fmt/SKILL.md diff --git a/.claude/skills/go-fmt/SKILL.md b/.claude/skills/go-fmt/SKILL.md new file mode 100644 index 0000000..71bd547 --- /dev/null +++ b/.claude/skills/go-fmt/SKILL.md @@ -0,0 +1,52 @@ +--- +name: go-fmt +description: > + Runs go fmt on all Go source files that were modified as part of the current + code change. Invoke this skill after completing any code changes to Go files + to ensure consistent formatting before committing. +--- + +# Go Fmt Skill + +## When to invoke + +Run this skill after any code change that creates or modifies one or more `.go` files. + +## Steps + +1. **Identify changed Go files** — run the following command to list every `.go` file that differs from the last commit: + + ```bash + git diff --name-only HEAD | grep '\.go$' + ``` + + If there are no staged/unstaged changes yet (e.g. files were just written), list all `.go` files that were touched during this session instead. + +2. **Run go fmt** — format all changed files in one call: + + ```bash + go fmt ./... + ``` + + This is preferred over per-file invocation because it also catches any indirectly affected files in the same packages. + +3. **Verify the result** — confirm that `go fmt` exited with code `0`. If it exited with a non-zero code, read the error output, fix the root cause (usually a syntax error in the modified file), and re-run `go fmt ./...`. + +4. **Check for reformatted files** — run: + + ```bash + git diff --name-only + ``` + + Any files listed here were reformatted by `go fmt`. This is expected and correct. + +5. **Report the outcome** — summarise what happened in one or two lines: + - If files were reformatted: list them and note that formatting was applied. + - If no files changed after `go fmt`: state "All changed Go files were already correctly formatted." + - If an error occurred: report the error message and the file it came from. + +## Notes + +- `go fmt` is a no-op on already-correctly-formatted code, so it is always safe to run. +- Do **not** skip this skill for test files (`*_test.go`) — they must be formatted too. +- The module root for this repository is `/home/runner/work/ScopeGuardian/ScopeGuardian` (or the directory where `go.mod` lives). Always run `go fmt` from that directory so the `./...` pattern covers all packages. From c2e268f7bb31bcaa2b568019cb5a76122b49131f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 18:12:17 +0000 Subject: [PATCH 13/17] feat: rename syft_depth, CVE from epss.cve, quote excludes, group_by component Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/0ad383c6-89b6-4809-9da8-19d656cc4642 Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- README.md | 6 +-- connectors/defectdojo/client/client.go | 2 +- connectors/defectdojo/const.go | 18 ++++---- connectors/defectdojo/factory_test.go | 2 +- connectors/defectdojo/service.go | 4 +- display/display.go | 4 +- display/display_test.go | 2 +- domains/models/finding.go | 22 +++++---- engine/const.go | 8 ++-- engine/engine.go | 10 ++--- engine/engine_test.go | 2 +- features/scans/grype/const.go | 8 ++-- features/scans/grype/dto.go | 13 ++++-- .../working_results/results/grype-result.json | 5 ++- features/scans/grype/service.go | 17 ++++--- features/scans/grype/service_test.go | 8 ++-- features/scans/kics/const.go | 20 ++++----- features/scans/kics/dto.go | 18 ++++---- features/scans/kics/service.go | 10 ++--- features/scans/kics/service_test.go | 4 +- features/scans/opengrep/const.go | 12 ++--- features/scans/opengrep/service.go | 22 ++++----- features/scans/opengrep/service_test.go | 6 +-- features/scans/syft/factory.go | 16 ++++--- features/scans/syft/factory_test.go | 16 +++---- features/scans/syft/service.go | 45 ++++++++++--------- features/scans/syft/service_test.go | 18 ++++---- features/security-gate/security_gate.go | 2 +- features/sync/sync.go | 5 +-- loader/dto.go | 10 ++--- loader/loader.go | 2 +- loader/loader_test.go | 2 +- loader/mocks/config_with_grype.toml | 2 +- main.go | 2 +- parser/parser.go | 14 +++--- 35 files changed, 189 insertions(+), 168 deletions(-) diff --git a/README.md b/README.md index aaa1a59..2fa2fe5 100644 --- a/README.md +++ b/README.md @@ -158,8 +158,8 @@ transitive_libraries = false # Note: test-scoped pom.xml dependencies are not affected (Syft has no Maven scope filter). # syft_exclude = ["**/src/test/**"] # Depth to recursively resolve parent POMs (env: SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH). -# 0 means no limit (the default). -# syft_max_parent_recursive_depth = 0 +# Default is 1; set to 0 for no limit. +# syft_depth = 1 # OpenGrep – static application security testing (SAST) scanner. [opengrep] @@ -190,7 +190,7 @@ transitive_libraries = false | `[grype].transitive_libraries` | bool | no | When `true`, Syft resolves transitive Java dependencies via Maven Central. Default: `false`. | | `[grype].exclude` | string array | no | Path glob patterns to exclude from Grype scanning (e.g. `["**/vendor/**"]`). | | `[grype].syft_exclude` | string array | no | Path glob patterns passed to Syft via `--exclude` during SBOM generation (e.g. `["**/src/test/**"]`). Excludes filesystem paths only; test-scoped `pom.xml` dependencies are unaffected. | -| `[grype].syft_max_parent_recursive_depth` | int | no | Maximum number of parent POM levels Syft will recursively resolve during Java/Maven analysis. `0` (default) means no limit. Forwarded as `SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH`. | +| `[grype].syft_depth` | int | no | Maximum number of parent POM levels Syft will recursively resolve during Java/Maven analysis. Default is `1`; `0` means no limit. Forwarded as `SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH`. | | `[opengrep].exclude` | string array | no | Path glob patterns to exclude from OpenGrep scanning (e.g. `["**/vendor/**"]`). | | `[opengrep].exclude_rule` | string array | no | OpenGrep rule IDs to skip (e.g. `["python.lang.security.audit.formatted-sql-query.formatted-sql-query"]`). | | `[proxy].http_proxy` | string | no | HTTP proxy URL forwarded as `HTTP_PROXY` / `http_proxy` to all scanner sub-processes. | diff --git a/connectors/defectdojo/client/client.go b/connectors/defectdojo/client/client.go index a4d4928..d74a5f3 100644 --- a/connectors/defectdojo/client/client.go +++ b/connectors/defectdojo/client/client.go @@ -1,11 +1,11 @@ package client import ( + "ScopeGuardian/logger" "bytes" "fmt" "io" "net/http" - "ScopeGuardian/logger" ) // Client is the interface for making HTTP requests to the DefectDojo API. diff --git a/connectors/defectdojo/const.go b/connectors/defectdojo/const.go index 4a92711..3c3a758 100644 --- a/connectors/defectdojo/const.go +++ b/connectors/defectdojo/const.go @@ -1,17 +1,17 @@ package defectdojo const ( - APIPrefix = "/api/v2" - GetProductsPath = "/products?name_exact=" - GetEngagementsPath = "/engagements?product=%d&offset=%d&limit=%d" - CreateEngagementPath = "/engagements/" - UpdateEngagementPath = "/engagements/%d/" - ImportScanPath = "/import-scan/" - ReimportScanPath = "/reimport-scan/" - GetTestsPath = "/tests/?engagement=%d&scan_type=%s" + APIPrefix = "/api/v2" + GetProductsPath = "/products?name_exact=" + GetEngagementsPath = "/engagements?product=%d&offset=%d&limit=%d" + CreateEngagementPath = "/engagements/" + UpdateEngagementPath = "/engagements/%d/" + ImportScanPath = "/import-scan/" + ReimportScanPath = "/reimport-scan/" + GetTestsPath = "/tests/?engagement=%d&scan_type=%s" // GetFindingsPath fetches only active (non-duplicate) findings and is used for // polling until the post-import count stabilises. - GetFindingsPath = "/findings/?test__engagement=%d&active=true&offset=%d&limit=%d" + GetFindingsPath = "/findings/?test__engagement=%d&active=true&offset=%d&limit=%d" // GetAllEngagementFindingsPath fetches all findings for an engagement regardless // of their active/duplicate status so that callers can read the "active" and // "duplicate" fields directly and derive the correct local Status. diff --git a/connectors/defectdojo/factory_test.go b/connectors/defectdojo/factory_test.go index c7abff7..9c0e5a9 100644 --- a/connectors/defectdojo/factory_test.go +++ b/connectors/defectdojo/factory_test.go @@ -1,8 +1,8 @@ package defectdojo import ( - "net/http" "ScopeGuardian/connectors/defectdojo/client" + "net/http" "testing" "github.com/stretchr/testify/assert" diff --git a/connectors/defectdojo/service.go b/connectors/defectdojo/service.go index cebddcf..9991026 100644 --- a/connectors/defectdojo/service.go +++ b/connectors/defectdojo/service.go @@ -1,6 +1,8 @@ package defectdojo import ( + "ScopeGuardian/connectors/defectdojo/client" + "ScopeGuardian/logger" "bytes" "encoding/json" "errors" @@ -9,8 +11,6 @@ import ( "net/http" "net/url" "reflect" - "ScopeGuardian/connectors/defectdojo/client" - "ScopeGuardian/logger" "strconv" "time" ) diff --git a/display/display.go b/display/display.go index e9a9eec..162096e 100644 --- a/display/display.go +++ b/display/display.go @@ -1,13 +1,13 @@ package display import ( + "ScopeGuardian/domains/models" + environment_variable "ScopeGuardian/environnement_variable" "encoding/csv" "encoding/json" "fmt" "io" "strconv" - "ScopeGuardian/domains/models" - environment_variable "ScopeGuardian/environnement_variable" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" diff --git a/display/display_test.go b/display/display_test.go index 7183723..a795332 100644 --- a/display/display_test.go +++ b/display/display_test.go @@ -1,10 +1,10 @@ package display import ( + "ScopeGuardian/domains/models" "bytes" "encoding/json" "strings" - "ScopeGuardian/domains/models" "testing" "github.com/stretchr/testify/assert" diff --git a/domains/models/finding.go b/domains/models/finding.go index 85e096a..c13541b 100644 --- a/domains/models/finding.go +++ b/domains/models/finding.go @@ -10,11 +10,11 @@ import ( const ( // FindingStatusActive indicates a finding that is active in DefectDojo // (not a duplicate, not suppressed). Without --sync all findings default to ACTIVE. - FindingStatusActive = "ACTIVE" + FindingStatusActive = "ACTIVE" // FindingStatusInactive indicates a finding that DefectDojo has suppressed, // marked as a false positive, or accepted as a risk. The local scanner still // reports it but it is excluded from security-gate evaluation. - FindingStatusInactive = "INACTIVE" + FindingStatusInactive = "INACTIVE" // FindingStatusDuplicate indicates a finding that DefectDojo's deduplication // engine has identified as a duplicate of another finding in the product. // Duplicate findings are excluded from security-gate evaluation. @@ -23,10 +23,14 @@ const ( // Finding represents a single security finding produced by a scanner. type Finding struct { - Engine string - Severity string - Name string - VulnId string + Engine string + Severity string + Name string + VulnId string + // Cwe holds the CWE identifier for most scanners (e.g. "CWE-79"). For Grype + // findings it holds the CVE identifier (extracted from vulnerability.epss.cve + // when present, falling back to vulnerability.id). The display layer renders + // this column as "CWE/CVE". Cwe string Description string SinkFile string @@ -77,9 +81,9 @@ func FilterFindingsByStatus(findings []Finding, statuses []string) []Finding { // Scanner-specific notes: // - Grype: recommendation is the "Upgrade to X" string derived from fix.versions. // - OpenGrep: recommendation is always "" because DefectDojo's Semgrep parser stores -// extra.message in description, not mitigation. The hash is additionally -// injected into extra.fingerprint before upload so that DefectDojo stores -// it as unique_id_from_tool, enabling a direct lookup without recomputation. +// extra.message in description, not mitigation. The hash is additionally +// injected into extra.fingerprint before upload so that DefectDojo stores +// it as unique_id_from_tool, enabling a direct lookup without recomputation. // - KICS: recommendation is the expected_value from each file entry. func ComputeFindingHash(severity, sinkFile string, sinkLine int, recommendation string) string { input := strings.ToLower(strings.TrimSpace(severity)) + "|" + diff --git a/engine/const.go b/engine/const.go index 9b5f091..8f38dca 100644 --- a/engine/const.go +++ b/engine/const.go @@ -24,8 +24,8 @@ const ( ) const ( - kicsScannerName = "Kics (IACST)" - syftScannerName = "Syft (SBOM)" - grypeScannerName = "Grype (SCA)" - opengrepScannerName = "OpenGrep (SAST)" + kicsScannerName = "Kics (IACST)" + syftScannerName = "Syft (SBOM)" + grypeScannerName = "Grype (SCA)" + opengrepScannerName = "OpenGrep (SAST)" ) diff --git a/engine/engine.go b/engine/engine.go index 46d7a48..1724fd5 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -1,22 +1,22 @@ package engine import ( - "fmt" - "net/http" - "os" - "path/filepath" "ScopeGuardian/connectors/defectdojo" "ScopeGuardian/connectors/defectdojo/client" "ScopeGuardian/domains/interfaces" "ScopeGuardian/domains/models" environment_variable "ScopeGuardian/environnement_variable" - featuresync "ScopeGuardian/features/sync" "ScopeGuardian/features/scans/grype" "ScopeGuardian/features/scans/kics" "ScopeGuardian/features/scans/opengrep" "ScopeGuardian/features/scans/syft" + featuresync "ScopeGuardian/features/sync" "ScopeGuardian/loader" "ScopeGuardian/logger" + "fmt" + "net/http" + "os" + "path/filepath" "sync" ) diff --git a/engine/engine_test.go b/engine/engine_test.go index 4a81ae6..1e981a7 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -1,11 +1,11 @@ package engine import ( - "errors" "ScopeGuardian/connectors/defectdojo" "ScopeGuardian/domains/interfaces" "ScopeGuardian/domains/models" "ScopeGuardian/loader" + "errors" "testing" "github.com/stretchr/testify/assert" diff --git a/features/scans/grype/const.go b/features/scans/grype/const.go index b8a8892..178baa5 100644 --- a/features/scans/grype/const.go +++ b/features/scans/grype/const.go @@ -1,16 +1,16 @@ package grype const ( - binaryPath = "/opt/grype/bin/grype" - dirPath = "/opt/grype" - configPath = "/opt/grype/config/grype.yaml" + binaryPath = "/opt/grype/bin/grype" + dirPath = "/opt/grype" + configPath = "/opt/grype/config/grype.yaml" outputFolder = "results" scannerType = "SCA" ) const ( severityThreshold = "Info" - groupByProperty = "finding_title" + groupByProperty = "component_name+component_version" findingGroupProperty = true findingTagProperty = true SCAEngineTag = "SCA" diff --git a/features/scans/grype/dto.go b/features/scans/grype/dto.go index 508fd6c..7214951 100644 --- a/features/scans/grype/dto.go +++ b/features/scans/grype/dto.go @@ -5,11 +5,16 @@ type GrypeFix struct { State string `json:"state"` } +type GrypeEpss struct { + Cve string `json:"cve"` +} + type GrypeVulnerability struct { - ID string `json:"id"` - Severity string `json:"severity"` - Description string `json:"description"` - Fix GrypeFix `json:"fix"` + ID string `json:"id"` + Severity string `json:"severity"` + Description string `json:"description"` + Fix GrypeFix `json:"fix"` + Epss *GrypeEpss `json:"epss"` } type GrypeArtifactLocation struct { diff --git a/features/scans/grype/mocks/working_results/results/grype-result.json b/features/scans/grype/mocks/working_results/results/grype-result.json index 6179c62..7c7c3e8 100644 --- a/features/scans/grype/mocks/working_results/results/grype-result.json +++ b/features/scans/grype/mocks/working_results/results/grype-result.json @@ -2,12 +2,15 @@ "matches": [ { "vulnerability": { - "id": "CVE-2021-1234", + "id": "GHSA-xxxx-1234", "severity": "High", "description": "A test high severity vulnerability in test-package", "fix": { "versions": ["1.2.0"], "state": "fixed" + }, + "epss": { + "cve": "CVE-2021-1234" } }, "artifact": { diff --git a/features/scans/grype/service.go b/features/scans/grype/service.go index 4fbd1c3..827562b 100644 --- a/features/scans/grype/service.go +++ b/features/scans/grype/service.go @@ -1,10 +1,6 @@ package grype import ( - "encoding/json" - "errors" - "fmt" - "os" "ScopeGuardian/connectors/defectdojo" "ScopeGuardian/domains/interfaces" "ScopeGuardian/domains/models" @@ -12,6 +8,10 @@ import ( "ScopeGuardian/exec" "ScopeGuardian/loader" "ScopeGuardian/logger" + "encoding/json" + "errors" + "fmt" + "os" "strings" "time" ) @@ -101,13 +101,18 @@ func (s *GrypeServiceImpl) LoadFindings() ([]models.Finding, error) { recommendation = fmt.Sprintf(recommendationUpgradeMultiple, strings.Join(match.Vulnerability.Fix.Versions, ", ")) } + cveId := match.Vulnerability.ID + if match.Vulnerability.Epss != nil && match.Vulnerability.Epss.Cve != "" { + cveId = match.Vulnerability.Epss.Cve + } + severity := strings.ToUpper(match.Vulnerability.Severity) f := models.Finding{ Engine: scannerType, Severity: severity, Name: fmt.Sprintf("%s %s", match.Artifact.Name, match.Artifact.Version), - VulnId: match.Vulnerability.ID, - Cwe: match.Vulnerability.ID, + VulnId: cveId, + Cwe: cveId, Description: match.Vulnerability.Description, SinkFile: sinkFile, Recommendation: recommendation, diff --git a/features/scans/grype/service_test.go b/features/scans/grype/service_test.go index 6df3aca..546dde7 100644 --- a/features/scans/grype/service_test.go +++ b/features/scans/grype/service_test.go @@ -1,12 +1,12 @@ package grype import ( - "fmt" - "os" "ScopeGuardian/connectors/defectdojo" "ScopeGuardian/domains/interfaces" environment_variable "ScopeGuardian/environnement_variable" "ScopeGuardian/loader" + "fmt" + "os" "testing" "github.com/stretchr/testify/assert" @@ -91,7 +91,7 @@ func TestNewGrypeService(t *testing.T) { } func TestLoadFindings(t *testing.T) { - t.Run("Should load findings", func(t *testing.T) { + t.Run("Should load findings and use epss.cve when present, fallback to vulnerability id otherwise", func(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() @@ -102,12 +102,14 @@ func TestLoadFindings(t *testing.T) { assert.Nil(t, err) assert.EqualValues(t, 2, len(findings)) + // finding[0]: vulnerability.id is a GHSA id; epss.cve holds the CVE assert.EqualValues(t, "test-package 1.0.0", findings[0].Name) assert.EqualValues(t, "CVE-2021-1234", findings[0].VulnId) assert.EqualValues(t, "CVE-2021-1234", findings[0].Cwe) assert.EqualValues(t, "HIGH", findings[0].Severity) assert.EqualValues(t, "Upgrade to 1.2.0", findings[0].Recommendation) + // finding[1]: no epss object; falls back to vulnerability.id assert.EqualValues(t, "another-package 2.0.0", findings[1].Name) assert.EqualValues(t, "CVE-2021-5678", findings[1].VulnId) assert.EqualValues(t, "CVE-2021-5678", findings[1].Cwe) diff --git a/features/scans/kics/const.go b/features/scans/kics/const.go index fad7aef..7ee83ce 100644 --- a/features/scans/kics/const.go +++ b/features/scans/kics/const.go @@ -18,16 +18,16 @@ const ( doNotReactivate = true ) const ( - scanArgument = "scan" - ciArgument = "--ci" - librariesPathArgument = "--libraries-path" - queriesPathArgument = "--queries-path" - outputPathArgument = "--output-path" - outputNameArgument = "--output-name" - pathArgument = "--path" - typeArgument = "--type" - ignoreOnExitArgument = "--ignore-on-exit" - excludeQueriesArgument = "--exclude-queries" + scanArgument = "scan" + ciArgument = "--ci" + librariesPathArgument = "--libraries-path" + queriesPathArgument = "--queries-path" + outputPathArgument = "--output-path" + outputNameArgument = "--output-name" + pathArgument = "--path" + typeArgument = "--type" + ignoreOnExitArgument = "--ignore-on-exit" + excludeQueriesArgument = "--exclude-queries" ) const ( diff --git a/features/scans/kics/dto.go b/features/scans/kics/dto.go index 250a22d..a500375 100644 --- a/features/scans/kics/dto.go +++ b/features/scans/kics/dto.go @@ -1,15 +1,15 @@ package kics type KicsFinding struct { - QueryName string `json:"query_name"` - QueryId string `json:"query_id"` - QueryUrl string `json:"query_url"` - Severity string `json:"severity"` - Platform string `json:"platforme"` - Cwe string `json:"cwe"` - RiskScore string `json:"risk_score"` - Description string `json:"description"` - Files []KicsFile `json:"files"` + QueryName string `json:"query_name"` + QueryId string `json:"query_id"` + QueryUrl string `json:"query_url"` + Severity string `json:"severity"` + Platform string `json:"platforme"` + Cwe string `json:"cwe"` + RiskScore string `json:"risk_score"` + Description string `json:"description"` + Files []KicsFile `json:"files"` } type KicsFile struct { diff --git a/features/scans/kics/service.go b/features/scans/kics/service.go index 71c86d9..461e7be 100644 --- a/features/scans/kics/service.go +++ b/features/scans/kics/service.go @@ -1,11 +1,6 @@ package kics import ( - "encoding/json" - "errors" - "fmt" - "io" - "os" "ScopeGuardian/connectors/defectdojo" "ScopeGuardian/domains/interfaces" "ScopeGuardian/domains/models" @@ -13,6 +8,11 @@ import ( "ScopeGuardian/exec" "ScopeGuardian/loader" "ScopeGuardian/logger" + "encoding/json" + "errors" + "fmt" + "io" + "os" "strings" "time" ) diff --git a/features/scans/kics/service_test.go b/features/scans/kics/service_test.go index 48e207e..9bae999 100644 --- a/features/scans/kics/service_test.go +++ b/features/scans/kics/service_test.go @@ -1,12 +1,12 @@ package kics import ( - "fmt" - "os" "ScopeGuardian/connectors/defectdojo" "ScopeGuardian/domains/interfaces" environment_variable "ScopeGuardian/environnement_variable" "ScopeGuardian/loader" + "fmt" + "os" "testing" "github.com/stretchr/testify/assert" diff --git a/features/scans/opengrep/const.go b/features/scans/opengrep/const.go index 9590180..8bb1f1e 100644 --- a/features/scans/opengrep/const.go +++ b/features/scans/opengrep/const.go @@ -22,12 +22,12 @@ const ( ) const ( - jsonOutputArgument = "--json-output=" - ossOnlyArgument = "--oss-only" - quietArgument = "-q" - skipUnknownExtArgument = "--skip-unknown-extensions" - excludeArgument = "--exclude" - excludeRuleArgument = "--exclude-rule" + jsonOutputArgument = "--json-output=" + ossOnlyArgument = "--oss-only" + quietArgument = "-q" + skipUnknownExtArgument = "--skip-unknown-extensions" + excludeArgument = "--exclude" + excludeRuleArgument = "--exclude-rule" ) const ( diff --git a/features/scans/opengrep/service.go b/features/scans/opengrep/service.go index 46cd526..30b5c93 100644 --- a/features/scans/opengrep/service.go +++ b/features/scans/opengrep/service.go @@ -1,11 +1,6 @@ package opengrep import ( - "encoding/json" - "errors" - "fmt" - "io" - "os" "ScopeGuardian/connectors/defectdojo" "ScopeGuardian/domains/interfaces" "ScopeGuardian/domains/models" @@ -13,6 +8,11 @@ import ( "ScopeGuardian/exec" "ScopeGuardian/loader" "ScopeGuardian/logger" + "encoding/json" + "errors" + "fmt" + "io" + "os" "strings" "time" ) @@ -109,12 +109,12 @@ func (s *OpenGrepServiceImpl) LoadFindings() ([]models.Finding, error) { description := strings.Join(item.Extra.Metadata.Owasp, ", ") f := models.Finding{ - Engine: scannerType, - Severity: severity, - Name: item.CheckId, - VulnId: item.CheckId, - Cwe: cwe, - Description: description, + Engine: scannerType, + Severity: severity, + Name: item.CheckId, + VulnId: item.CheckId, + Cwe: cwe, + Description: description, // Message is kept for display but intentionally excluded from the hash: // DefectDojo's Semgrep parser stores extra.message in description, not // mitigation, so f.Mitigation from the DD API will be empty for these findings. diff --git a/features/scans/opengrep/service_test.go b/features/scans/opengrep/service_test.go index 84e9dac..a06a650 100644 --- a/features/scans/opengrep/service_test.go +++ b/features/scans/opengrep/service_test.go @@ -1,14 +1,14 @@ package opengrep import ( - "encoding/json" - "fmt" - "os" "ScopeGuardian/connectors/defectdojo" "ScopeGuardian/domains/interfaces" "ScopeGuardian/domains/models" environment_variable "ScopeGuardian/environnement_variable" "ScopeGuardian/loader" + "encoding/json" + "fmt" + "os" "testing" "github.com/stretchr/testify/assert" diff --git a/features/scans/syft/factory.go b/features/scans/syft/factory.go index 0c1359b..4c32584 100644 --- a/features/scans/syft/factory.go +++ b/features/scans/syft/factory.go @@ -7,18 +7,20 @@ import ( // GetSyftService constructs and returns a ScanServiceImpl for the Syft SBOM generator // using the provided loader configuration. When a Grype configuration is present, -// its TransitiveLibraries flag, SyftExclude patterns, and SyftMaxParentRecursiveDepth -// are forwarded to the Syft service to control whether transitive Java dependencies -// are resolved from Maven Central, which filesystem paths are excluded during SBOM -// generation, and how many parent POM levels are recursively resolved. +// its TransitiveLibraries flag, SyftExclude patterns, and SyftDepth are forwarded +// to the Syft service to control whether transitive Java dependencies are resolved +// from Maven Central, which filesystem paths are excluded during SBOM generation, +// and how many parent POM levels are recursively resolved. func GetSyftService(config loader.Config) interfaces.ScanServiceImpl { transitiveLibraries := false var exclude []string - maxParentRecursiveDepth := 0 + depth := 1 if config.Grype != nil { transitiveLibraries = config.Grype.TransitiveLibraries exclude = config.Grype.SyftExclude - maxParentRecursiveDepth = config.Grype.SyftMaxParentRecursiveDepth + if config.Grype.SyftDepth != 0 { + depth = config.Grype.SyftDepth + } } - return newSyftService(config.Path, transitiveLibraries, exclude, maxParentRecursiveDepth, config.Proxy.ToEnv()) + return newSyftService(config.Path, transitiveLibraries, exclude, depth, config.Proxy.ToEnv()) } diff --git a/features/scans/syft/factory_test.go b/features/scans/syft/factory_test.go index bc98b2f..f31ce17 100644 --- a/features/scans/syft/factory_test.go +++ b/features/scans/syft/factory_test.go @@ -52,30 +52,30 @@ func TestGetSyftService(t *testing.T) { assert.Equal(t, patterns, svc.exclude) }) - t.Run("Should use syft_max_parent_recursive_depth from grype config when zero", func(t *testing.T) { - service := GetSyftService(loader.Config{Grype: &loader.Grype{SyftMaxParentRecursiveDepth: 0}}) + t.Run("Should use default syft_depth of 1 when grype config has depth zero (not configured)", func(t *testing.T) { + service := GetSyftService(loader.Config{Grype: &loader.Grype{SyftDepth: 0}}) svc, ok := service.(*SyftServiceImpl) assert.NotNil(t, service) assert.True(t, ok) - assert.Equal(t, 0, svc.maxParentRecursiveDepth) + assert.Equal(t, 1, svc.depth) }) - t.Run("Should use syft_max_parent_recursive_depth from grype config when set", func(t *testing.T) { - service := GetSyftService(loader.Config{Grype: &loader.Grype{SyftMaxParentRecursiveDepth: 5}}) + t.Run("Should use syft_depth from grype config when set", func(t *testing.T) { + service := GetSyftService(loader.Config{Grype: &loader.Grype{SyftDepth: 5}}) svc, ok := service.(*SyftServiceImpl) assert.NotNil(t, service) assert.True(t, ok) - assert.Equal(t, 5, svc.maxParentRecursiveDepth) + assert.Equal(t, 5, svc.depth) }) - t.Run("Should default syft_max_parent_recursive_depth to 0 when grype config is nil", func(t *testing.T) { + t.Run("Should default syft_depth to 1 when grype config is nil", func(t *testing.T) { service := GetSyftService(loader.Config{}) svc, ok := service.(*SyftServiceImpl) assert.NotNil(t, service) assert.True(t, ok) - assert.Equal(t, 0, svc.maxParentRecursiveDepth) + assert.Equal(t, 1, svc.depth) }) } diff --git a/features/scans/syft/service.go b/features/scans/syft/service.go index 9cfb938..9be4533 100644 --- a/features/scans/syft/service.go +++ b/features/scans/syft/service.go @@ -1,16 +1,16 @@ package syft import ( - "errors" - "fmt" - "io" - "os" "ScopeGuardian/connectors/defectdojo" "ScopeGuardian/domains/interfaces" "ScopeGuardian/domains/models" environment_variable "ScopeGuardian/environnement_variable" "ScopeGuardian/exec" "ScopeGuardian/logger" + "errors" + "fmt" + "io" + "os" "strings" ) @@ -20,32 +20,33 @@ type execRunner func(binaryPath string, dirPath string, args []string, stdout io // SyftServiceImpl implements ScanServiceImpl for the Syft SBOM generator. type SyftServiceImpl struct { - path string - transitiveLibraries bool - exclude []string - maxParentRecursiveDepth int - proxyEnv []string - runner execRunner + path string + transitiveLibraries bool + exclude []string + depth int + proxyEnv []string + runner execRunner } // newSyftService builds a SyftServiceImpl from the scan path, resolving it // relative to the SCAN_DIR environment variable. transitiveLibraries controls // whether Syft resolves transitive Java dependencies from Maven Central. // exclude is a list of glob patterns passed to Syft via --exclude to skip -// matching paths during SBOM generation (e.g. ["**/src/test/**"]). -// maxParentRecursiveDepth sets how many parent POM levels Syft will resolve; -// 0 means no limit (the default). It is forwarded as the +// matching paths during SBOM generation (e.g. ["**/src/test/**"]); each +// pattern is quoted in the CLI invocation. +// depth sets how many parent POM levels Syft will resolve; 0 means no limit, +// 1 is the default. It is forwarded as the // SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH environment variable. // proxyEnv is an optional list of "KEY=VALUE" proxy environment variable entries // (see loader.Proxy.ToEnv) forwarded to the Syft process. -func newSyftService(path string, transitiveLibraries bool, exclude []string, maxParentRecursiveDepth int, proxyEnv []string) interfaces.ScanServiceImpl { +func newSyftService(path string, transitiveLibraries bool, exclude []string, depth int, proxyEnv []string) interfaces.ScanServiceImpl { return &SyftServiceImpl{ - path: fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], path), - transitiveLibraries: transitiveLibraries, - exclude: exclude, - maxParentRecursiveDepth: maxParentRecursiveDepth, - proxyEnv: proxyEnv, - runner: exec.Wrap, + path: fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], path), + transitiveLibraries: transitiveLibraries, + exclude: exclude, + depth: depth, + proxyEnv: proxyEnv, + runner: exec.Wrap, } } @@ -71,7 +72,7 @@ func (s *SyftServiceImpl) Start() (bool, error) { } for _, pattern := range s.exclude { - args = append(args, excludeArgument, pattern) + args = append(args, excludeArgument, fmt.Sprintf(`"%s"`, pattern)) } logger.Info(fmt.Sprintf(logInfoCommandLine, strings.Join(args, " "))) @@ -80,7 +81,7 @@ func (s *SyftServiceImpl) Start() (bool, error) { extraEnv := []string{ fmt.Sprintf("%s=%s", envJavaUseNetwork, transitiveValue), fmt.Sprintf("%s=%s", envJavaResolveTransitiveDependencies, transitiveValue), - fmt.Sprintf("%s=%d", envJavaMaxParentRecursiveDepth, s.maxParentRecursiveDepth), + fmt.Sprintf("%s=%d", envJavaMaxParentRecursiveDepth, s.depth), } extraEnv = append(extraEnv, s.proxyEnv...) return s.runner(binaryPath, dirPath, args, os.Stdout, os.Stderr, extraEnv...) diff --git a/features/scans/syft/service_test.go b/features/scans/syft/service_test.go index ae948d7..88a9103 100644 --- a/features/scans/syft/service_test.go +++ b/features/scans/syft/service_test.go @@ -1,11 +1,11 @@ package syft import ( + "ScopeGuardian/domains/interfaces" + environment_variable "ScopeGuardian/environnement_variable" "fmt" "io" "os" - "ScopeGuardian/domains/interfaces" - environment_variable "ScopeGuardian/environnement_variable" "testing" "github.com/stretchr/testify/assert" @@ -63,13 +63,13 @@ func TestSyftStart(t *testing.T) { assert.True(t, ok) }) - t.Run("Should pass each exclude pattern as a separate --exclude arg", func(t *testing.T) { + t.Run("Should pass each exclude pattern as a separate --exclude arg (quoted)", func(t *testing.T) { _ = os.Setenv("SCAN_DIR", os.TempDir()) environment_variable.ReloadEnv() patterns := []string{"**/src/test/**", "**/testdata/**"} var capturedArgs []string - svc := newSyftService(".", false, patterns, 0, nil).(*SyftServiceImpl) + svc := newSyftService(".", false, patterns, 1, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, args []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { capturedArgs = args return true, nil @@ -81,7 +81,7 @@ func TestSyftStart(t *testing.T) { assert.True(t, ok) assert.Contains(t, capturedArgs, excludeArgument) for _, p := range patterns { - assert.Contains(t, capturedArgs, p) + assert.Contains(t, capturedArgs, fmt.Sprintf(`"%s"`, p)) } }) @@ -103,7 +103,7 @@ func TestSyftStart(t *testing.T) { assert.NotContains(t, capturedArgs, excludeArgument) }) - t.Run("Should pass SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH env var with configured value", func(t *testing.T) { + t.Run("Should pass SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH env var with configured depth", func(t *testing.T) { _ = os.Setenv("SCAN_DIR", os.TempDir()) environment_variable.ReloadEnv() @@ -121,12 +121,12 @@ func TestSyftStart(t *testing.T) { assert.Contains(t, capturedEnv, "SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH=3") }) - t.Run("Should pass SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH=0 when default value is used", func(t *testing.T) { + t.Run("Should pass SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH=1 when default depth is used", func(t *testing.T) { _ = os.Setenv("SCAN_DIR", os.TempDir()) environment_variable.ReloadEnv() var capturedEnv []string - svc := newSyftService(".", false, nil, 0, nil).(*SyftServiceImpl) + svc := newSyftService(".", false, nil, 1, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, _ []string, _ io.Writer, _ io.Writer, extraEnv ...string) (bool, error) { capturedEnv = extraEnv return true, nil @@ -136,7 +136,7 @@ func TestSyftStart(t *testing.T) { assert.Nil(t, err) assert.True(t, ok) - assert.Contains(t, capturedEnv, "SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH=0") + assert.Contains(t, capturedEnv, "SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH=1") }) } diff --git a/features/security-gate/security_gate.go b/features/security-gate/security_gate.go index 13a7fbf..508b9ce 100644 --- a/features/security-gate/security_gate.go +++ b/features/security-gate/security_gate.go @@ -5,10 +5,10 @@ package securitygate import ( - "fmt" "ScopeGuardian/domains/models" "ScopeGuardian/logger" "ScopeGuardian/parser" + "fmt" "strings" ) diff --git a/features/sync/sync.go b/features/sync/sync.go index 76e23e4..2641486 100644 --- a/features/sync/sync.go +++ b/features/sync/sync.go @@ -1,11 +1,11 @@ package sync import ( - "errors" - "fmt" "ScopeGuardian/connectors/defectdojo" "ScopeGuardian/domains/models" "ScopeGuardian/logger" + "errors" + "fmt" "time" ) @@ -243,4 +243,3 @@ func MarkFindingsByDDFindings(local []models.Finding, ddFindings []defectdojo.Fi } return result } - diff --git a/loader/dto.go b/loader/dto.go index 9587f17..15c0acf 100644 --- a/loader/dto.go +++ b/loader/dto.go @@ -36,11 +36,11 @@ type ( // projects, dev dependencies are already excluded unconditionally via the // include-dev-dependencies: false setting in syft.yaml. SyftExclude []string `toml:"syft_exclude"` - // SyftMaxParentRecursiveDepth controls how many levels of parent POMs Syft - // will recursively resolve during Java/Maven analysis. A value of 0 (the - // default) means no limit. Passed to Syft as the - // SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH environment variable. - SyftMaxParentRecursiveDepth int `toml:"syft_max_parent_recursive_depth"` + // SyftDepth controls how many levels of parent POMs Syft will recursively + // resolve during Java/Maven analysis. Defaults to 1 when unset (0). + // Passed to Syft as the SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH environment + // variable. + SyftDepth int `toml:"syft_depth"` } // Opengrep contains the configuration for the OpenGrep SAST scanner. diff --git a/loader/loader.go b/loader/loader.go index a2bb901..bfa6a01 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -1,10 +1,10 @@ package loader import ( + "ScopeGuardian/logger" "errors" "fmt" "os" - "ScopeGuardian/logger" "github.com/pelletier/go-toml/v2" ) diff --git a/loader/loader_test.go b/loader/loader_test.go index c20f7d9..85ca483 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -28,7 +28,7 @@ func TestLoad(t *testing.T) { assert.Nil(t, config.Kics) assert.EqualValues(t, "not-fixed,unknown,wont-fix", config.Grype.IgnoreStates) assert.EqualValues(t, []string{"/etc/**"}, config.Grype.Exclude) - assert.EqualValues(t, 5, config.Grype.SyftMaxParentRecursiveDepth) + assert.EqualValues(t, 5, config.Grype.SyftDepth) }) t.Run("Should load configuration file without engine", func(t *testing.T) { diff --git a/loader/mocks/config_with_grype.toml b/loader/mocks/config_with_grype.toml index cc953f1..2437f2a 100644 --- a/loader/mocks/config_with_grype.toml +++ b/loader/mocks/config_with_grype.toml @@ -4,4 +4,4 @@ path = "./" [grype] ignore_states = "not-fixed,unknown,wont-fix" exclude = ["/etc/**"] -syft_max_parent_recursive_depth = 5 +syft_depth = 5 diff --git a/main.go b/main.go index ba52ea9..1dfeac4 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "os" "ScopeGuardian/display" "ScopeGuardian/domains/models" "ScopeGuardian/engine" @@ -9,6 +8,7 @@ import ( "ScopeGuardian/loader" "ScopeGuardian/logger" "ScopeGuardian/parser" + "os" "golang.org/x/exp/slog" ) diff --git a/parser/parser.go b/parser/parser.go index 3696592..6b97634 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -45,14 +45,14 @@ func PrintUsage(w io.Writer) { func Parse(args []string) (Args, error) { fs := flag.NewFlagSet("ScopeGuardian", flag.ContinueOnError) - sync := fs.Bool("sync", false, "Enable sync result with DefectDojo") - threshold := fs.String("threshold", "", "Enable security gate (e.g., critical=1,high=2)") + sync := fs.Bool("sync", false, "Enable sync result with DefectDojo") + threshold := fs.String("threshold", "", "Enable security gate (e.g., critical=1,high=2)") projectName := fs.String("projectName", "", "Name of the project to scan") - branch := fs.String("branch", "", "Project branch to scan") - quiet := fs.Bool("q", false, "Quiet mode: suppress all log output") - output := fs.String("o", "", "Write findings to the specified file") - format := fs.String("format", FormatJSON, "Output format for -o: json, csv, or raw") - filter := fs.String("filter", FilterActive, "Comma-separated finding statuses to display: ACTIVE, INACTIVE, DUPLICATE (default: ACTIVE)") + branch := fs.String("branch", "", "Project branch to scan") + quiet := fs.Bool("q", false, "Quiet mode: suppress all log output") + output := fs.String("o", "", "Write findings to the specified file") + format := fs.String("format", FormatJSON, "Output format for -o: json, csv, or raw") + filter := fs.String("filter", FilterActive, "Comma-separated finding statuses to display: ACTIVE, INACTIVE, DUPLICATE (default: ACTIVE)") if err := fs.Parse(args); err != nil { return Args{}, err From e4ecafa13ba96157846be6a8c6cf9dee4690163a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 18:15:56 +0000 Subject: [PATCH 14/17] fix: use single quotes for syft --exclude patterns Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/11f4f143-afb1-43ec-80ad-1cef94a6633f Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- features/scans/syft/service.go | 2 +- features/scans/syft/service_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/scans/syft/service.go b/features/scans/syft/service.go index 9be4533..f6e419c 100644 --- a/features/scans/syft/service.go +++ b/features/scans/syft/service.go @@ -72,7 +72,7 @@ func (s *SyftServiceImpl) Start() (bool, error) { } for _, pattern := range s.exclude { - args = append(args, excludeArgument, fmt.Sprintf(`"%s"`, pattern)) + args = append(args, excludeArgument, fmt.Sprintf(`'%s'`, pattern)) } logger.Info(fmt.Sprintf(logInfoCommandLine, strings.Join(args, " "))) diff --git a/features/scans/syft/service_test.go b/features/scans/syft/service_test.go index 88a9103..b03390a 100644 --- a/features/scans/syft/service_test.go +++ b/features/scans/syft/service_test.go @@ -81,7 +81,7 @@ func TestSyftStart(t *testing.T) { assert.True(t, ok) assert.Contains(t, capturedArgs, excludeArgument) for _, p := range patterns { - assert.Contains(t, capturedArgs, fmt.Sprintf(`"%s"`, p)) + assert.Contains(t, capturedArgs, fmt.Sprintf(`'%s'`, p)) } }) From ada570c1b466019f0763db6acdf6ab7df51d7129 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 18:17:51 +0000 Subject: [PATCH 15/17] fix(syft): remove quote wrapping from --exclude patterns in exec.Command call Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/a47021be-4ce7-466f-a1d6-ecf71a32c569 Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- features/scans/syft/service.go | 2 +- features/scans/syft/service_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/scans/syft/service.go b/features/scans/syft/service.go index f6e419c..81ca170 100644 --- a/features/scans/syft/service.go +++ b/features/scans/syft/service.go @@ -72,7 +72,7 @@ func (s *SyftServiceImpl) Start() (bool, error) { } for _, pattern := range s.exclude { - args = append(args, excludeArgument, fmt.Sprintf(`'%s'`, pattern)) + args = append(args, excludeArgument, pattern) } logger.Info(fmt.Sprintf(logInfoCommandLine, strings.Join(args, " "))) diff --git a/features/scans/syft/service_test.go b/features/scans/syft/service_test.go index b03390a..c6689ca 100644 --- a/features/scans/syft/service_test.go +++ b/features/scans/syft/service_test.go @@ -81,7 +81,7 @@ func TestSyftStart(t *testing.T) { assert.True(t, ok) assert.Contains(t, capturedArgs, excludeArgument) for _, p := range patterns { - assert.Contains(t, capturedArgs, fmt.Sprintf(`'%s'`, p)) + assert.Contains(t, capturedArgs, p) } }) From ebbabe7065bea0765c5ced90c647644a1f40df90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 18:24:28 +0000 Subject: [PATCH 16/17] fix: update GrypeEpss to slice to match new Grype JSON array schema Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/ead3cdcd-f8f2-4ee4-81ba-05692f723f5a Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- features/scans/grype/dto.go | 10 +- .../working_results/results/grype-result.json | 96 ++++++++++--------- features/scans/grype/service.go | 4 +- 3 files changed, 56 insertions(+), 54 deletions(-) diff --git a/features/scans/grype/dto.go b/features/scans/grype/dto.go index 7214951..1c0e6ac 100644 --- a/features/scans/grype/dto.go +++ b/features/scans/grype/dto.go @@ -10,11 +10,11 @@ type GrypeEpss struct { } type GrypeVulnerability struct { - ID string `json:"id"` - Severity string `json:"severity"` - Description string `json:"description"` - Fix GrypeFix `json:"fix"` - Epss *GrypeEpss `json:"epss"` + ID string `json:"id"` + Severity string `json:"severity"` + Description string `json:"description"` + Fix GrypeFix `json:"fix"` + Epss []GrypeEpss `json:"epss"` } type GrypeArtifactLocation struct { diff --git a/features/scans/grype/mocks/working_results/results/grype-result.json b/features/scans/grype/mocks/working_results/results/grype-result.json index 7c7c3e8..cb496b5 100644 --- a/features/scans/grype/mocks/working_results/results/grype-result.json +++ b/features/scans/grype/mocks/working_results/results/grype-result.json @@ -1,49 +1,51 @@ { - "matches": [ - { - "vulnerability": { - "id": "GHSA-xxxx-1234", - "severity": "High", - "description": "A test high severity vulnerability in test-package", - "fix": { - "versions": ["1.2.0"], - "state": "fixed" - }, - "epss": { - "cve": "CVE-2021-1234" - } - }, - "artifact": { - "name": "test-package", - "version": "1.0.0", - "type": "npm", - "locations": [ - { - "path": "/app/package.json" - } - ] - } - }, - { - "vulnerability": { - "id": "CVE-2021-5678", - "severity": "Medium", - "description": "A test medium severity vulnerability in another-package", - "fix": { - "versions": [], - "state": "not-fixed" - } - }, - "artifact": { - "name": "another-package", - "version": "2.0.0", - "type": "npm", - "locations": [ - { - "path": "/app/package.json" - } - ] - } - } - ] +"matches": [ +{ +"vulnerability": { +"id": "GHSA-xxxx-1234", +"severity": "High", +"description": "A test high severity vulnerability in test-package", +"fix": { +"versions": ["1.2.0"], +"state": "fixed" +}, +"epss": [ +{ +"cve": "CVE-2021-1234" +} +] +}, +"artifact": { +"name": "test-package", +"version": "1.0.0", +"type": "npm", +"locations": [ +{ +"path": "/app/package.json" +} +] +} +}, +{ +"vulnerability": { +"id": "CVE-2021-5678", +"severity": "Medium", +"description": "A test medium severity vulnerability in another-package", +"fix": { +"versions": [], +"state": "not-fixed" +} +}, +"artifact": { +"name": "another-package", +"version": "2.0.0", +"type": "npm", +"locations": [ +{ +"path": "/app/package.json" +} +] +} +} +] } diff --git a/features/scans/grype/service.go b/features/scans/grype/service.go index 827562b..03bd513 100644 --- a/features/scans/grype/service.go +++ b/features/scans/grype/service.go @@ -102,8 +102,8 @@ func (s *GrypeServiceImpl) LoadFindings() ([]models.Finding, error) { } cveId := match.Vulnerability.ID - if match.Vulnerability.Epss != nil && match.Vulnerability.Epss.Cve != "" { - cveId = match.Vulnerability.Epss.Cve + if len(match.Vulnerability.Epss) > 0 && match.Vulnerability.Epss[0].Cve != "" { + cveId = match.Vulnerability.Epss[0].Cve } severity := strings.ToUpper(match.Vulnerability.Severity) From 61901c185892f085abb0547929472b0d7b5517a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 18:25:14 +0000 Subject: [PATCH 17/17] fix: restore tab indentation in grype mock JSON fixture Agent-Logs-Url: https://github.com/ParanoiHack/ScopeGuardian/sessions/ead3cdcd-f8f2-4ee4-81ba-05692f723f5a Co-authored-by: Nitr4x <10544300+Nitr4x@users.noreply.github.com> --- .../working_results/results/grype-result.json | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/features/scans/grype/mocks/working_results/results/grype-result.json b/features/scans/grype/mocks/working_results/results/grype-result.json index cb496b5..f0c7f9e 100644 --- a/features/scans/grype/mocks/working_results/results/grype-result.json +++ b/features/scans/grype/mocks/working_results/results/grype-result.json @@ -1,51 +1,51 @@ { -"matches": [ -{ -"vulnerability": { -"id": "GHSA-xxxx-1234", -"severity": "High", -"description": "A test high severity vulnerability in test-package", -"fix": { -"versions": ["1.2.0"], -"state": "fixed" -}, -"epss": [ -{ -"cve": "CVE-2021-1234" -} -] -}, -"artifact": { -"name": "test-package", -"version": "1.0.0", -"type": "npm", -"locations": [ -{ -"path": "/app/package.json" -} -] -} -}, -{ -"vulnerability": { -"id": "CVE-2021-5678", -"severity": "Medium", -"description": "A test medium severity vulnerability in another-package", -"fix": { -"versions": [], -"state": "not-fixed" -} -}, -"artifact": { -"name": "another-package", -"version": "2.0.0", -"type": "npm", -"locations": [ -{ -"path": "/app/package.json" -} -] -} -} -] + "matches": [ + { + "vulnerability": { + "id": "GHSA-xxxx-1234", + "severity": "High", + "description": "A test high severity vulnerability in test-package", + "fix": { + "versions": ["1.2.0"], + "state": "fixed" + }, + "epss": [ + { + "cve": "CVE-2021-1234" + } + ] + }, + "artifact": { + "name": "test-package", + "version": "1.0.0", + "type": "npm", + "locations": [ + { + "path": "/app/package.json" + } + ] + } + }, + { + "vulnerability": { + "id": "CVE-2021-5678", + "severity": "Medium", + "description": "A test medium severity vulnerability in another-package", + "fix": { + "versions": [], + "state": "not-fixed" + } + }, + "artifact": { + "name": "another-package", + "version": "2.0.0", + "type": "npm", + "locations": [ + { + "path": "/app/package.json" + } + ] + } + } + ] }