diff --git a/README.md b/README.md index 56a6d8c..a403ed4 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,13 @@ path = "./my-service" # exclude = ["**/vendor/**", "**/testdata/**"] # Optional list of rule IDs to skip. # exclude_rule = ["python.lang.security.audit.formatted-sql-query.formatted-sql-query"] + +# Proxy – optional HTTP/HTTPS proxy settings forwarded to all scanner sub-processes. +# All three fields are optional. Omit the entire section or leave fields empty to disable. +# [proxy] +# http_proxy = "http://proxy.company.com:3128" +# https_proxy = "http://proxy.company.com:3128" +# no_proxy = "localhost,127.0.0.1" ``` ### Fields Reference @@ -177,6 +184,10 @@ path = "./my-service" | `[opengrep].path` | string | yes* | Path to the directory to scan. Resolved as `$SCAN_DIR/`. | | `[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. | +| `[proxy].https_proxy` | string | no | HTTPS proxy URL forwarded as `HTTPS_PROXY` / `https_proxy` to all scanner sub-processes. | +| `[proxy].no_proxy` | string | no | Comma-separated list of hosts that bypass the proxy, forwarded as `NO_PROXY` / `no_proxy`. | +| `[proxy].ssl_cert_file` | string | no | Path to a PEM-encoded CA certificate bundle forwarded as `SSL_CERT_FILE` (Go tools) and `REQUESTS_CA_BUNDLE` (Python tools such as OpenGrep) to all scanner sub-processes. Required when using an intercepting proxy (e.g. Burp Suite). | \* Required only if you want KICS scanning to run. Omitting the entire `[kics]` section disables the scanner. @@ -184,6 +195,10 @@ Omitting the entire `[grype]` section disables both Grype and the Syft SBOM gene Omitting the entire `[opengrep]` section disables the SAST scanner. +Omitting the entire `[proxy]` section (or leaving all fields empty) disables proxy forwarding — scanner sub-processes inherit no proxy environment variables from this configuration. + +Both the uppercase (`HTTP_PROXY`) and lowercase (`http_proxy`) variants of each proxy variable are set for maximum compatibility across tools. The `ssl_cert_file` value is emitted as both `SSL_CERT_FILE` (used by Go-based tools) and `REQUESTS_CA_BUNDLE` (used by Python-based tools such as OpenGrep). + --- ## Environment Variables @@ -407,6 +422,27 @@ docker run --rm \ Inside the container `SCAN_DIR` defaults to `/tmp/data`. +> **HTTPS proxy requirement — `--cap-add SYS_PTRACE`** +> +> OpenGrep bundles a Python interpreter inside its binary and reads `/proc/1/map_files` at startup to bootstrap it. In a Docker container this path requires the `CAP_SYS_PTRACE` Linux capability, which is dropped by default. If you run ScopeGuardian behind an HTTPS proxy (i.e. `[proxy].https_proxy` is set), you must add this capability so that OpenGrep can start: +> +> ```bash +> docker run --rm --cap-add SYS_PTRACE \ +> -v /path/to/your/project:/tmp/data/project \ +> -v /path/to/config.toml:/config.toml \ +> -e SCAN_DIR=/tmp/data \ +> -e DD_URL=http://host.docker.internal:8080 \ +> -e DD_ACCESS_TOKEN= \ +> ScopeGuardian \ +> --projectName my-service \ +> --branch main \ +> --sync \ +> /config.toml +> ``` +> +> With Docker Compose add `cap_add: [SYS_PTRACE]` to the ScopeGuardian service. +> With GitHub Actions use `options: --cap-add SYS_PTRACE` inside the `container:` block. + --- ## Local DefectDojo Setup diff --git a/config.toml b/config.toml index 03f4011..ed8b350 100644 --- a/config.toml +++ b/config.toml @@ -13,3 +13,11 @@ transitive_libraries = false [opengrep] exclude = ["tests/**"] exclude_rule = [] + +# Optional proxy configuration forwarded to scanner sub-processes (Grype, OpenGrep, Syft, KICS). +# Uncomment and set the values below if the scanners must reach the internet through a proxy. +# [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" # PEM-encoded CA certificate (e.g. Burp Suite CA) diff --git a/engine/const.go b/engine/const.go index 2bff3f1..9b5f091 100644 --- a/engine/const.go +++ b/engine/const.go @@ -5,6 +5,7 @@ const ( logInfoSyftRegister = "Grype enabled. Registering Syft SBOM scanner for execution" logInfoGrypeRegister = "Grype enabled. Registering Grype vulnerability scanner for execution" logInfoOpenGrepRegister = "OpenGrep enabled. Registering OpenGrep SAST scanner for execution" + logInfoCreateResultsDir = "Creating results directory" logInfoScannerStarting = "Starting %s scanning engine" logInfoScannerSuccess = "%s scanner succeeded" logInfoFindingsLoaded = "%s findings loaded" @@ -19,6 +20,7 @@ const ( logErrorRetrieveEngagementId = "Cannot retrieve engagement ID for project [%s] branch [%s]" logErrorSkippingScanner = "Skipping %s scanner because prerequisite %s failed" logErrorSyncResult = "Failed to sync %s results to DefectDojo" + logErrorCreateResultsDir = "Failed to create results directory" ) const ( diff --git a/engine/engine.go b/engine/engine.go index bc942db..46d7a48 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -3,6 +3,8 @@ package engine import ( "fmt" "net/http" + "os" + "path/filepath" "ScopeGuardian/connectors/defectdojo" "ScopeGuardian/connectors/defectdojo/client" "ScopeGuardian/domains/interfaces" @@ -45,7 +47,10 @@ func NewEngine() *Engine { // Initialize reads the provided configuration and registers any scanner whose // section is present and non-empty. Syft is registered as a prerequisite for // Grype so that it always runs and completes before Grype starts. -func (e *Engine) Initialize(config loader.Config) { +// It also creates the results directory under SCAN_DIR so that all scanners +// can write their output without having to manage the folder themselves. +// Returns an error if the results directory cannot be created. +func (e *Engine) Initialize(config loader.Config) error { if config.Kics != nil { logger.Info(logInfoKicsRegister) e.registerScanner(kicsScannerName, kics.GetKicsService(config)) @@ -62,6 +67,15 @@ func (e *Engine) Initialize(config loader.Config) { logger.Info(logInfoOpenGrepRegister) e.registerScanner(opengrepScannerName, opengrep.GetOpenGrepService(config)) } + + logger.Info(logInfoCreateResultsDir) + resultsDir := filepath.Join(environment_variable.EnvironmentVariable["SCAN_DIR"], "results") + if err := os.MkdirAll(resultsDir, 0755); err != nil { + logger.Error(logErrorCreateResultsDir, logger.Err(err)) + return err + } + + return nil } // Start runs all registered scanners in two phases: diff --git a/engine/engine_test.go b/engine/engine_test.go index ecea793..4a81ae6 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -146,7 +146,8 @@ func TestInitialize(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, config) - engine.Initialize(config) + err = engine.Initialize(config) + assert.Nil(t, err) assert.EqualValues(t, 1, len(engine.scanners)) }) @@ -158,7 +159,8 @@ func TestInitialize(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, config) - engine.Initialize(config) + err = engine.Initialize(config) + assert.Nil(t, err) assert.EqualValues(t, 1, len(engine.prerequisites)) assert.EqualValues(t, 1, len(engine.scanners)) @@ -176,7 +178,8 @@ func TestInitialize(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, config) - engine.Initialize(config) + err = engine.Initialize(config) + assert.Nil(t, err) assert.EqualValues(t, 0, len(engine.scanners)) }) @@ -188,7 +191,8 @@ func TestInitialize(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, config) - engine.Initialize(config) + err = engine.Initialize(config) + assert.Nil(t, err) assert.EqualValues(t, 1, len(engine.scanners)) _, ok := engine.scanners[opengrepScannerName] diff --git a/features/scans/grype/factory.go b/features/scans/grype/factory.go index beb66ac..b1efbf2 100644 --- a/features/scans/grype/factory.go +++ b/features/scans/grype/factory.go @@ -8,5 +8,5 @@ import ( // GetGrypeService constructs and returns a ScanServiceImpl for the Grype vulnerability scanner // using the provided loader configuration. func GetGrypeService(config loader.Config) interfaces.ScanServiceImpl { - return newGrypeService(*config.Grype) + return newGrypeService(*config.Grype, config.Proxy.ToEnv()) } diff --git a/features/scans/grype/service.go b/features/scans/grype/service.go index 04d8e30..9499675 100644 --- a/features/scans/grype/service.go +++ b/features/scans/grype/service.go @@ -23,16 +23,20 @@ type GrypeServiceImpl struct { output string ignoreStates string exclude []string + proxyEnv []string } // newGrypeService builds a GrypeServiceImpl from the Grype loader configuration, // resolving the SBOM input and result output paths from the SCAN_DIR environment variable. -func newGrypeService(config loader.Grype) interfaces.ScanServiceImpl { +// proxyEnv is an optional list of "KEY=VALUE" proxy environment variable entries +// (see loader.Proxy.ToEnv) forwarded to the Grype process. +func newGrypeService(config loader.Grype, proxyEnv []string) interfaces.ScanServiceImpl { return &GrypeServiceImpl{ sbom: fmt.Sprintf("%s/%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], outputFolder, sbomInputNameParameter), output: fmt.Sprintf("%s/%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], outputFolder, outputNameParameter), ignoreStates: config.IgnoreStates, exclude: config.Exclude, + proxyEnv: proxyEnv, } } @@ -64,7 +68,7 @@ func (s *GrypeServiceImpl) Start() (bool, error) { logger.Info(fmt.Sprintf(logInfoCommandLine, strings.Join(args, " "))) - return exec.Wrap(binaryPath, dirPath, args, os.Stdout, os.Stderr) + return exec.Wrap(binaryPath, dirPath, args, os.Stdout, os.Stderr, s.proxyEnv...) } // LoadFindings reads the Grype JSON output file and converts each vulnerability diff --git a/features/scans/grype/service_test.go b/features/scans/grype/service_test.go index a718d0f..74d276d 100644 --- a/features/scans/grype/service_test.go +++ b/features/scans/grype/service_test.go @@ -69,7 +69,7 @@ var _ defectdojo.DefectDojoService = &mockDefectDojoService{} // value that satisfies the interfaces.ScanServiceImpl contract, enforcing that // all required scanner methods are present at compile and test time. func TestNewGrypeServiceImplementsInterface(t *testing.T) { - service := newGrypeService(loader.Grype{}) + service := newGrypeService(loader.Grype{}, nil) _, ok := service.(interfaces.ScanServiceImpl) assert.True(t, ok) @@ -80,7 +80,7 @@ func TestNewGrypeService(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "")) environment_variable.ReloadEnv() - service := newGrypeService(loader.Grype{}) + service := newGrypeService(loader.Grype{}, nil) ok, err := service.Start() @@ -95,7 +95,7 @@ func TestLoadFindings(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newGrypeService(loader.Grype{}) + service := newGrypeService(loader.Grype{}, nil) findings, err := service.LoadFindings() @@ -117,7 +117,7 @@ func TestLoadFindings(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "")) environment_variable.ReloadEnv() - service := newGrypeService(loader.Grype{}) + service := newGrypeService(loader.Grype{}, nil) findings, err := service.LoadFindings() @@ -129,7 +129,7 @@ func TestLoadFindings(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/bad_format_results")) environment_variable.ReloadEnv() - service := newGrypeService(loader.Grype{}) + service := newGrypeService(loader.Grype{}, nil) findings, err := service.LoadFindings() @@ -143,7 +143,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newGrypeService(loader.Grype{}) + service := newGrypeService(loader.Grype{}, nil) ddMock := &mockDefectDojoService{importScanOk: true, importScanErr: nil} err := service.Sync(1, "main", ddMock) @@ -155,7 +155,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newGrypeService(loader.Grype{}) + service := newGrypeService(loader.Grype{}, nil) ddMock := &mockDefectDojoService{ testsToReturn: []defectdojo.Test{{Id: 3, ScanType: "Anchore Grype"}}, reimportScanOk: true, @@ -172,7 +172,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newGrypeService(loader.Grype{}) + service := newGrypeService(loader.Grype{}, nil) ddMock := &mockDefectDojoService{importScanOk: false, importScanErr: fmt.Errorf("import failed")} err := service.Sync(1, "main", ddMock) @@ -184,7 +184,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newGrypeService(loader.Grype{}) + service := newGrypeService(loader.Grype{}, nil) ddMock := &mockDefectDojoService{ testsToReturn: []defectdojo.Test{{Id: 3, ScanType: "Anchore Grype"}}, reimportScanOk: false, @@ -200,7 +200,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newGrypeService(loader.Grype{}) + service := newGrypeService(loader.Grype{}, nil) ddMock := &mockDefectDojoService{getTestsErr: fmt.Errorf("cannot retrieve tests")} err := service.Sync(1, "main", ddMock) @@ -212,7 +212,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "")) environment_variable.ReloadEnv() - service := newGrypeService(loader.Grype{}) + service := newGrypeService(loader.Grype{}, nil) ddMock := &mockDefectDojoService{} err := service.Sync(1, "main", ddMock) diff --git a/features/scans/kics/factory.go b/features/scans/kics/factory.go index e62419b..eb7b226 100644 --- a/features/scans/kics/factory.go +++ b/features/scans/kics/factory.go @@ -8,5 +8,5 @@ import ( // GetKicsService constructs and returns a ScanServiceImpl for the KICS scanner // using the provided loader configuration. func GetKicsService(config loader.Config) interfaces.ScanServiceImpl { - return newKicsService(config.Path, *config.Kics) + return newKicsService(config.Path, *config.Kics, config.Proxy.ToEnv()) } diff --git a/features/scans/kics/service.go b/features/scans/kics/service.go index 10de126..71c86d9 100644 --- a/features/scans/kics/service.go +++ b/features/scans/kics/service.go @@ -23,16 +23,20 @@ type KicsServiceImpl struct { platform string output string excludeQueries []string + proxyEnv []string } // newKicsService builds a KicsServiceImpl from the scan path and loader configuration, // resolving the scan path and output file path relative to the SCAN_DIR environment variable. -func newKicsService(path string, config loader.Kics) interfaces.ScanServiceImpl { +// proxyEnv is an optional list of "KEY=VALUE" proxy environment variable entries +// (see loader.Proxy.ToEnv) forwarded to the KICS process. +func newKicsService(path string, config loader.Kics, proxyEnv []string) interfaces.ScanServiceImpl { return &KicsServiceImpl{ path: fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], path), output: fmt.Sprintf("%s/%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], outputFolder, outputNameParameter), platform: config.Platform, excludeQueries: config.ExcludeQueries, + proxyEnv: proxyEnv, } } @@ -83,7 +87,7 @@ func (s *KicsServiceImpl) Start() (bool, error) { logger.Info(fmt.Sprintf(logInfoCommandLine, strings.Join(args, " "))) - return exec.Wrap(binaryPath, dirPath, args, io.Discard, io.Discard) + return exec.Wrap(binaryPath, dirPath, args, io.Discard, io.Discard, s.proxyEnv...) } // LoadFindings reads the KICS JSON output file and converts each query result diff --git a/features/scans/kics/service_test.go b/features/scans/kics/service_test.go index e1cb942..48e207e 100644 --- a/features/scans/kics/service_test.go +++ b/features/scans/kics/service_test.go @@ -69,14 +69,14 @@ var _ defectdojo.DefectDojoService = &mockDefectDojoService{} // value that satisfies the interfaces.ScanServiceImpl contract, enforcing that // all required scanner methods are present at compile and test time. func TestNewKicsServiceImplementsInterface(t *testing.T) { - service := newKicsService("./test", loader.Kics{}) + service := newKicsService("./test", loader.Kics{}, nil) _, ok := service.(interfaces.ScanServiceImpl) assert.True(t, ok) } func TestNewKicsService(t *testing.T) { - service := newKicsService("./test", loader.Kics{ExcludeQueries: []string{"a227ec01-f97a-4084-91a4-47b350c1db54"}}) + service := newKicsService("./test", loader.Kics{ExcludeQueries: []string{"a227ec01-f97a-4084-91a4-47b350c1db54"}}, nil) impl, ok := service.(*KicsServiceImpl) assert.True(t, ok) @@ -102,7 +102,7 @@ func TestLoadFinding(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newKicsService("./test", loader.Kics{}) + service := newKicsService("./test", loader.Kics{}, nil) findings, err := service.LoadFindings() @@ -114,7 +114,7 @@ func TestLoadFinding(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "")) environment_variable.ReloadEnv() - service := newKicsService("./test", loader.Kics{}) + service := newKicsService("./test", loader.Kics{}, nil) findings, err := service.LoadFindings() @@ -126,7 +126,7 @@ func TestLoadFinding(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/bad_format_results")) environment_variable.ReloadEnv() - service := newKicsService("./test", loader.Kics{}) + service := newKicsService("./test", loader.Kics{}, nil) findings, err := service.LoadFindings() @@ -148,7 +148,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newKicsService("./test", loader.Kics{}) + service := newKicsService("./test", loader.Kics{}, nil) ddMock := &mockDefectDojoService{importScanOk: true, importScanErr: nil} err := service.Sync(1, "main", ddMock) @@ -160,7 +160,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newKicsService("./test", loader.Kics{}) + service := newKicsService("./test", loader.Kics{}, nil) ddMock := &mockDefectDojoService{ testsToReturn: []defectdojo.Test{{Id: 5, ScanType: "KICS Scan"}}, reimportScanOk: true, @@ -177,7 +177,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newKicsService("./test", loader.Kics{}) + service := newKicsService("./test", loader.Kics{}, nil) ddMock := &mockDefectDojoService{importScanOk: false, importScanErr: fmt.Errorf("import failed")} err := service.Sync(1, "main", ddMock) @@ -189,7 +189,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newKicsService("./test", loader.Kics{}) + service := newKicsService("./test", loader.Kics{}, nil) ddMock := &mockDefectDojoService{ testsToReturn: []defectdojo.Test{{Id: 5, ScanType: "KICS Scan"}}, reimportScanOk: false, @@ -205,7 +205,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newKicsService("./test", loader.Kics{}) + service := newKicsService("./test", loader.Kics{}, nil) ddMock := &mockDefectDojoService{getTestsErr: fmt.Errorf("cannot retrieve tests")} err := service.Sync(1, "main", ddMock) @@ -217,7 +217,7 @@ func TestSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "")) environment_variable.ReloadEnv() - service := newKicsService("./test", loader.Kics{}) + service := newKicsService("./test", loader.Kics{}, nil) ddMock := &mockDefectDojoService{} err := service.Sync(1, "main", ddMock) diff --git a/features/scans/opengrep/factory.go b/features/scans/opengrep/factory.go index e099878..c3fc302 100644 --- a/features/scans/opengrep/factory.go +++ b/features/scans/opengrep/factory.go @@ -8,5 +8,5 @@ import ( // GetOpenGrepService constructs and returns a ScanServiceImpl for the OpenGrep scanner // using the provided loader configuration. func GetOpenGrepService(config loader.Config) interfaces.ScanServiceImpl { - return newOpenGrepService(config.Path, *config.Opengrep) + return newOpenGrepService(config.Path, *config.Opengrep, config.Proxy.ToEnv()) } diff --git a/features/scans/opengrep/service.go b/features/scans/opengrep/service.go index 6fbfc51..46cd526 100644 --- a/features/scans/opengrep/service.go +++ b/features/scans/opengrep/service.go @@ -23,16 +23,20 @@ type OpenGrepServiceImpl struct { output string exclude []string excludeRule []string + proxyEnv []string } // newOpenGrepService builds an OpenGrepServiceImpl from the scan path and loader configuration, // resolving the scan path and output file path relative to the SCAN_DIR environment variable. -func newOpenGrepService(path string, config loader.Opengrep) interfaces.ScanServiceImpl { +// proxyEnv is an optional list of "KEY=VALUE" proxy environment variable entries +// (see loader.Proxy.ToEnv) forwarded to the OpenGrep process. +func newOpenGrepService(path string, config loader.Opengrep, proxyEnv []string) interfaces.ScanServiceImpl { return &OpenGrepServiceImpl{ path: fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], path), output: fmt.Sprintf("%s/%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], outputFolder, outputNameParameter), exclude: config.Exclude, excludeRule: config.ExcludeRule, + proxyEnv: proxyEnv, } } @@ -74,7 +78,7 @@ func (s *OpenGrepServiceImpl) Start() (bool, error) { logger.Info(fmt.Sprintf(logInfoCommandLine, strings.Join(args, " "))) - return exec.Wrap(binaryPath, dirPath, args, io.Discard, io.Discard) + return exec.Wrap(binaryPath, dirPath, args, io.Discard, io.Discard, s.proxyEnv...) } // LoadFindings reads the OpenGrep JSON output file and converts each result diff --git a/features/scans/opengrep/service_test.go b/features/scans/opengrep/service_test.go index 5144668..84e9dac 100644 --- a/features/scans/opengrep/service_test.go +++ b/features/scans/opengrep/service_test.go @@ -71,7 +71,7 @@ var _ defectdojo.DefectDojoService = &mockDefectDojoService{} // a value that satisfies the interfaces.ScanServiceImpl contract, enforcing that // all required scanner methods are present at compile and test time. func TestNewOpenGrepServiceImplementsInterface(t *testing.T) { - service := newOpenGrepService("./test", loader.Opengrep{}) + service := newOpenGrepService("./test", loader.Opengrep{}, nil) _, ok := service.(interfaces.ScanServiceImpl) assert.True(t, ok) @@ -96,7 +96,7 @@ func TestOpenGrepStart(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "")) environment_variable.ReloadEnv() - service := newOpenGrepService("./nonexistent", loader.Opengrep{}) + service := newOpenGrepService("./nonexistent", loader.Opengrep{}, nil) ok, err := service.Start() @@ -111,7 +111,7 @@ func TestOpenGrepLoadFindings(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newOpenGrepService("./test", loader.Opengrep{}) + service := newOpenGrepService("./test", loader.Opengrep{}, nil) findings, err := service.LoadFindings() @@ -137,7 +137,7 @@ func TestOpenGrepLoadFindings(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/string_metadata_results")) environment_variable.ReloadEnv() - service := newOpenGrepService("./test", loader.Opengrep{}) + service := newOpenGrepService("./test", loader.Opengrep{}, nil) findings, err := service.LoadFindings() @@ -152,7 +152,7 @@ func TestOpenGrepLoadFindings(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "")) environment_variable.ReloadEnv() - service := newOpenGrepService("./test", loader.Opengrep{}) + service := newOpenGrepService("./test", loader.Opengrep{}, nil) findings, err := service.LoadFindings() @@ -164,7 +164,7 @@ func TestOpenGrepLoadFindings(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/bad_format_results")) environment_variable.ReloadEnv() - service := newOpenGrepService("./test", loader.Opengrep{}) + service := newOpenGrepService("./test", loader.Opengrep{}, nil) findings, err := service.LoadFindings() @@ -268,7 +268,7 @@ func TestOpenGrepSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newOpenGrepService("./test", loader.Opengrep{}) + service := newOpenGrepService("./test", loader.Opengrep{}, nil) ddMock := &mockDefectDojoService{importScanOk: true, importScanErr: nil} err := service.Sync(1, "main", ddMock) @@ -280,7 +280,7 @@ func TestOpenGrepSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newOpenGrepService("./test", loader.Opengrep{}) + service := newOpenGrepService("./test", loader.Opengrep{}, nil) ddMock := &mockDefectDojoService{ testsToReturn: []defectdojo.Test{{Id: 7, ScanType: "Semgrep JSON Report"}}, reimportScanOk: true, @@ -297,7 +297,7 @@ func TestOpenGrepSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newOpenGrepService("./test", loader.Opengrep{}) + service := newOpenGrepService("./test", loader.Opengrep{}, nil) ddMock := &mockDefectDojoService{importScanOk: false, importScanErr: fmt.Errorf("import failed")} err := service.Sync(1, "main", ddMock) @@ -309,7 +309,7 @@ func TestOpenGrepSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newOpenGrepService("./test", loader.Opengrep{}) + service := newOpenGrepService("./test", loader.Opengrep{}, nil) ddMock := &mockDefectDojoService{ testsToReturn: []defectdojo.Test{{Id: 7, ScanType: "Semgrep JSON Report"}}, reimportScanOk: false, @@ -325,7 +325,7 @@ func TestOpenGrepSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "./mocks/working_results")) environment_variable.ReloadEnv() - service := newOpenGrepService("./test", loader.Opengrep{}) + service := newOpenGrepService("./test", loader.Opengrep{}, nil) ddMock := &mockDefectDojoService{getTestsErr: fmt.Errorf("cannot retrieve tests")} err := service.Sync(1, "main", ddMock) @@ -337,7 +337,7 @@ func TestOpenGrepSync(t *testing.T) { _ = os.Setenv("SCAN_DIR", fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["PWD"], "")) environment_variable.ReloadEnv() - service := newOpenGrepService("./test", loader.Opengrep{}) + service := newOpenGrepService("./test", loader.Opengrep{}, nil) ddMock := &mockDefectDojoService{} err := service.Sync(1, "main", ddMock) diff --git a/features/scans/syft/factory.go b/features/scans/syft/factory.go index eebf42a..accbba2 100644 --- a/features/scans/syft/factory.go +++ b/features/scans/syft/factory.go @@ -14,5 +14,5 @@ func GetSyftService(config loader.Config) interfaces.ScanServiceImpl { if config.Grype != nil { transitiveLibraries = config.Grype.TransitiveLibraries } - return newSyftService(config.Path, transitiveLibraries) + return newSyftService(config.Path, transitiveLibraries, config.Proxy.ToEnv()) } diff --git a/features/scans/syft/service.go b/features/scans/syft/service.go index 86b9de4..37b027b 100644 --- a/features/scans/syft/service.go +++ b/features/scans/syft/service.go @@ -22,16 +22,20 @@ type execRunner func(binaryPath string, dirPath string, args []string, stdout io type SyftServiceImpl struct { path string transitiveLibraries 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. -func newSyftService(path string, transitiveLibraries bool) interfaces.ScanServiceImpl { +// 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 { return &SyftServiceImpl{ path: fmt.Sprintf("%s/%s", environment_variable.EnvironmentVariable["SCAN_DIR"], path), transitiveLibraries: transitiveLibraries, + proxyEnv: proxyEnv, runner: exec.Wrap, } } @@ -60,10 +64,12 @@ func (s *SyftServiceImpl) Start() (bool, error) { logger.Info(fmt.Sprintf(logInfoCommandLine, strings.Join(args, " "))) transitiveValue := fmt.Sprintf("%v", s.transitiveLibraries) - return s.runner(binaryPath, dirPath, args, os.Stdout, os.Stderr, + extraEnv := []string{ fmt.Sprintf("%s=%s", envJavaUseNetwork, transitiveValue), fmt.Sprintf("%s=%s", envJavaResolveTransitiveDependencies, transitiveValue), - ) + } + extraEnv = append(extraEnv, s.proxyEnv...) + return s.runner(binaryPath, dirPath, args, os.Stdout, os.Stderr, extraEnv...) } // LoadFindings is intentionally empty: Syft is used only to produce the SBOM diff --git a/features/scans/syft/service_test.go b/features/scans/syft/service_test.go index 5426614..701a6d2 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) + service := newSyftService("./test", 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) + service := newSyftService("./doesnotexist", 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).(*SyftServiceImpl) + svc := newSyftService(".", 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).(*SyftServiceImpl) + svc := newSyftService(".", true, nil).(*SyftServiceImpl) svc.runner = func(_ string, _ string, _ []string, _ io.Writer, _ io.Writer, _ ...string) (bool, error) { return true, nil } @@ -66,7 +66,7 @@ func TestSyftStart(t *testing.T) { func TestSyftLoadFindings(t *testing.T) { t.Run("Should return nil findings and nil error", func(t *testing.T) { - service := newSyftService("./test", false) + service := newSyftService("./test", false, nil) findings, err := service.LoadFindings() @@ -77,7 +77,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) + service := newSyftService("./test", false, nil) err := service.Sync(1, "main", nil) diff --git a/loader/dto.go b/loader/dto.go index 4e956bd..fa0d3db 100644 --- a/loader/dto.go +++ b/loader/dto.go @@ -8,6 +8,7 @@ type ( Kics *Kics Grype *Grype Opengrep *Opengrep + Proxy *Proxy ProtectedBranches []string `toml:"protected_branches"` } @@ -33,4 +34,42 @@ type ( Exclude []string `toml:"exclude"` ExcludeRule []string `toml:"exclude_rule"` } + + // Proxy holds optional HTTP proxy settings forwarded to scanner sub-processes + // as HTTP_PROXY, HTTPS_PROXY, NO_PROXY, SSL_CERT_FILE, and REQUESTS_CA_BUNDLE + // environment variables. All fields are optional and are omitted from the child + // environment when empty. + Proxy struct { + HttpProxy string `toml:"http_proxy"` + HttpsProxy string `toml:"https_proxy"` + NoProxy string `toml:"no_proxy"` + SslCertFile string `toml:"ssl_cert_file"` + } ) + +// ToEnv converts the Proxy configuration into a list of "KEY=VALUE" environment +// variable entries suitable for passing as extraEnv to exec.Wrap / exec.WrapAllowExitCodes. +// Both the uppercase (HTTP_PROXY) and lowercase (http_proxy) variants are included +// for maximum compatibility across tools. SSL_CERT_FILE is also emitted as +// REQUESTS_CA_BUNDLE so that Python-based tools (e.g. OpenGrep) honour the same +// certificate. Returns nil when the receiver is nil or all fields are empty. +func (p *Proxy) ToEnv() []string { + if p == nil { + return nil + } + + var env []string + if p.HttpProxy != "" { + env = append(env, "HTTP_PROXY="+p.HttpProxy, "http_proxy="+p.HttpProxy) + } + if p.HttpsProxy != "" { + env = append(env, "HTTPS_PROXY="+p.HttpsProxy, "https_proxy="+p.HttpsProxy) + } + if p.NoProxy != "" { + env = append(env, "NO_PROXY="+p.NoProxy, "no_proxy="+p.NoProxy) + } + if p.SslCertFile != "" { + env = append(env, "SSL_CERT_FILE="+p.SslCertFile, "REQUESTS_CA_BUNDLE="+p.SslCertFile) + } + return env +} diff --git a/loader/dto_test.go b/loader/dto_test.go new file mode 100644 index 0000000..1e25201 --- /dev/null +++ b/loader/dto_test.go @@ -0,0 +1,58 @@ +package loader + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProxyToEnv(t *testing.T) { + t.Run("Should return nil for nil proxy", func(t *testing.T) { + var p *Proxy + assert.Nil(t, p.ToEnv()) + }) + + t.Run("Should return nil for empty proxy", func(t *testing.T) { + p := &Proxy{} + assert.Nil(t, p.ToEnv()) + }) + + t.Run("Should return HTTP_PROXY entries when http_proxy is set", func(t *testing.T) { + p := &Proxy{HttpProxy: "http://proxy.example.com:3128"} + env := p.ToEnv() + assert.Contains(t, env, "HTTP_PROXY=http://proxy.example.com:3128") + assert.Contains(t, env, "http_proxy=http://proxy.example.com:3128") + }) + + t.Run("Should return HTTPS_PROXY entries when https_proxy is set", func(t *testing.T) { + p := &Proxy{HttpsProxy: "http://proxy.example.com:3128"} + env := p.ToEnv() + assert.Contains(t, env, "HTTPS_PROXY=http://proxy.example.com:3128") + assert.Contains(t, env, "https_proxy=http://proxy.example.com:3128") + }) + + t.Run("Should return NO_PROXY entries when no_proxy is set", func(t *testing.T) { + p := &Proxy{NoProxy: "localhost,127.0.0.1"} + env := p.ToEnv() + assert.Contains(t, env, "NO_PROXY=localhost,127.0.0.1") + assert.Contains(t, env, "no_proxy=localhost,127.0.0.1") + }) + + t.Run("Should return SSL_CERT_FILE and REQUESTS_CA_BUNDLE entries when ssl_cert_file is set", func(t *testing.T) { + p := &Proxy{SslCertFile: "/etc/ssl/certs/burp-ca.pem"} + env := p.ToEnv() + assert.Contains(t, env, "SSL_CERT_FILE=/etc/ssl/certs/burp-ca.pem") + assert.Contains(t, env, "REQUESTS_CA_BUNDLE=/etc/ssl/certs/burp-ca.pem") + }) + + t.Run("Should return all eight entries when all fields are set", func(t *testing.T) { + p := &Proxy{ + HttpProxy: "http://proxy.example.com:3128", + HttpsProxy: "https://proxy.example.com:3128", + NoProxy: "localhost", + SslCertFile: "/etc/ssl/certs/burp-ca.pem", + } + env := p.ToEnv() + assert.Len(t, env, 8) + }) +} diff --git a/loader/loader_test.go b/loader/loader_test.go index 049897c..6c2d95d 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -53,6 +53,17 @@ func TestLoad(t *testing.T) { assert.EqualValues(t, []string{"python.lang.correctness"}, config.Opengrep.ExcludeRule) }) + t.Run("Should load configuration file with proxy section", func(t *testing.T) { + config, err := Load("./mocks/config_with_proxy.toml") + + assert.Nil(t, err) + assert.NotNil(t, config.Proxy) + assert.EqualValues(t, "http://proxy.example.com:3128", config.Proxy.HttpProxy) + assert.EqualValues(t, "http://proxy.example.com:3128", config.Proxy.HttpsProxy) + assert.EqualValues(t, "localhost,127.0.0.1", config.Proxy.NoProxy) + assert.EqualValues(t, "/etc/ssl/certs/burp-ca.pem", config.Proxy.SslCertFile) + }) + t.Run("Should not load configuration file cause wrong pathname", func(t *testing.T) { config, err := Load("./mocks/does_not_exist.toml") diff --git a/loader/mocks/config_with_proxy.toml b/loader/mocks/config_with_proxy.toml new file mode 100644 index 0000000..56c4c43 --- /dev/null +++ b/loader/mocks/config_with_proxy.toml @@ -0,0 +1,8 @@ +title = "Scope-guardian configuration file" +path = "./" + +[proxy] +http_proxy = "http://proxy.example.com:3128" +https_proxy = "http://proxy.example.com:3128" +no_proxy = "localhost,127.0.0.1" +ssl_cert_file = "/etc/ssl/certs/burp-ca.pem" diff --git a/main.go b/main.go index dd58600..ba52ea9 100644 --- a/main.go +++ b/main.go @@ -5,10 +5,10 @@ import ( "ScopeGuardian/display" "ScopeGuardian/domains/models" "ScopeGuardian/engine" + securitygate "ScopeGuardian/features/security-gate" "ScopeGuardian/loader" "ScopeGuardian/logger" "ScopeGuardian/parser" - securitygate "ScopeGuardian/features/security-gate" "golang.org/x/exp/slog" ) @@ -51,7 +51,11 @@ func main() { eng := engine.NewEngine() - eng.Initialize(config) + if err := eng.Initialize(config); err != nil { + logger.Error(err.Error()) + os.Exit(-2) + } + eng.Start() findings := eng.LoadFindings()