Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -177,13 +184,21 @@ path = "./my-service"
| `[opengrep].path` | string | yes* | Path to the directory to scan. Resolved as `$SCAN_DIR/<path>`. |
| `[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.

Omitting the entire `[grype]` section disables both Grype and the Syft SBOM generation step.

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
Expand Down Expand Up @@ -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=<your-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
Expand Down
8 changes: 8 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 2 additions & 0 deletions engine/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 (
Expand Down
16 changes: 15 additions & 1 deletion engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package engine
import (
"fmt"
"net/http"
"os"
"path/filepath"
"ScopeGuardian/connectors/defectdojo"
"ScopeGuardian/connectors/defectdojo/client"
"ScopeGuardian/domains/interfaces"
Expand Down Expand Up @@ -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))
Expand All @@ -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:
Expand Down
12 changes: 8 additions & 4 deletions engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
Expand All @@ -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))
Expand All @@ -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))
})
Expand All @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion features/scans/grype/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
8 changes: 6 additions & 2 deletions features/scans/grype/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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
Expand Down
22 changes: 11 additions & 11 deletions features/scans/grype/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()

Expand All @@ -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()

Expand All @@ -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()

Expand All @@ -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()

Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion features/scans/kics/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
8 changes: 6 additions & 2 deletions features/scans/kics/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading