diff --git a/cliv2/cmd/cliv2/main.go b/cliv2/cmd/cliv2/main.go index 8fb3890a69..3d126390eb 100644 --- a/cliv2/cmd/cliv2/main.go +++ b/cliv2/cmd/cliv2/main.go @@ -1,8 +1,6 @@ package main // !!! This import needs to be the first import, please do not change this !!! -import _ "github.com/snyk/go-application-framework/pkg/networking/fips_enable" - import ( "context" "encoding/json" @@ -25,19 +23,20 @@ import ( "github.com/snyk/cli-extension-mcp-scan/pkg/mcpscan" "github.com/snyk/cli-extension-os-flows/pkg/osflows" "github.com/snyk/cli-extension-sbom/pkg/sbom" - "github.com/snyk/container-cli/pkg/container" - "github.com/snyk/error-catalog-golang-public/cli" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/snyk/cli/cliv2/cmd/cliv2/behavior/legacy" "github.com/snyk/cli/cliv2/internal/cliv2" "github.com/snyk/cli/cliv2/internal/constants" + featureflaggateway "github.com/snyk/cli/cliv2/internal/feature-flag-gateway" + "github.com/snyk/container-cli/pkg/container" + "github.com/snyk/error-catalog-golang-public/cli" "github.com/snyk/go-application-framework/pkg/analytics" "github.com/snyk/go-application-framework/pkg/app" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/instrumentation" "github.com/snyk/go-application-framework/pkg/logging" + _ "github.com/snyk/go-application-framework/pkg/networking/fips_enable" + "github.com/spf13/cobra" + "github.com/spf13/pflag" cliv2utils "github.com/snyk/cli/cliv2/internal/utils" @@ -46,7 +45,6 @@ import ( "github.com/snyk/go-application-framework/pkg/local_workflows/network_utils" workflows "github.com/snyk/go-application-framework/pkg/local_workflows/connectivity_check_extension" - "github.com/snyk/go-httpauth/pkg/httpauth" "github.com/snyk/snyk-iac-capture/pkg/capture" @@ -59,7 +57,6 @@ import ( "github.com/snyk/go-application-framework/pkg/workflow" snykls "github.com/snyk/snyk-ls/ls_extension" - "github.com/snyk/studio-mcp/pkg/mcp" cli_errors "github.com/snyk/cli/cliv2/internal/errors" @@ -77,10 +74,11 @@ var scrubbedLogger logging.ScrubbingLogWriter var interactionId = instrumentation.AssembleUrnFromUUID(uuid.NewString()) const ( - unknownCommandMessage string = "unknown command" - disable_analytics_flag string = "DISABLE_ANALYTICS" - debug_level_flag string = "log-level" - integrationNameFlag string = "integration-name" + unknownCommandMessage string = "unknown command" + disable_analytics_flag string = "DISABLE_ANALYTICS" + debug_level_flag string = "log-level" + integrationNameFlag string = "integration-name" + showMavenBuildScopeFlag string = "show-maven-build-scope" ) type JsonErrorStruct struct { @@ -591,6 +589,14 @@ func MainWithErrorCode() int { // add workflows as commands createCommandsForWorkflows(rootCommand, globalEngine) + // fetch feature flags + ffgService, err := featureflaggateway.NewService(globalConfiguration.GetUrl(configuration.API_URL), 10*time.Second) + if err != nil { + globalLogger.Print("Failed to fetch feature flags", err) + return constants.SNYK_EXIT_CODE_ERROR + } + apiToken := globalConfiguration.GetString(configuration.AUTHENTICATION_TOKEN) + // init Analytics cliAnalytics := globalEngine.GetAnalytics() cliAnalytics.SetVersion(cliv2.GetFullVersion()) @@ -601,6 +607,8 @@ func MainWithErrorCode() int { cliAnalytics.GetInstrumentation().SetCategory(instrumentation.DetermineCategory(os.Args, globalEngine)) cliAnalytics.GetInstrumentation().SetStage(instrumentation.DetermineStage(cliAnalytics.IsCiEnvironment())) cliAnalytics.GetInstrumentation().SetStatus(analytics.Success) + cliAnalytics.GetInstrumentation().AddExtension(showMavenBuildScopeFlag, + IsFeatureEnabled(ctx, ffgService, globalConfiguration.GetString(configuration.ORGANIZATION), showMavenBuildScopeFlag, apiToken)) setTimeout(globalConfiguration, func() { os.Exit(constants.SNYK_EXIT_CODE_EX_UNAVAILABLE) @@ -659,6 +667,26 @@ func MainWithErrorCode() int { return exitCode } +func IsFeatureEnabled( + ctx context.Context, + service featureflaggateway.Service, + orgID string, + flag string, + token string, +) bool { + resp, err := service.EvaluateFlags(ctx, []string{flag}, orgID, "2024-10-15", token) + if err != nil || resp == nil { + return false + } + evals := resp.Data.Attributes.Evaluations + for _, e := range evals { + if e.Key == flag { + return e.Value + } + } + return false +} + func legacyCLITerminated(err error, errorList []error) error { exitErr, isExitError := err.(*exec.ExitError) if isExitError && exitErr.ExitCode() == constants.SNYK_EXIT_CODE_TS_CLI_TERMINATED { diff --git a/cliv2/go.mod b/cliv2/go.mod index 900f9a6658..c7b273b17d 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -60,7 +60,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect - github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/bubbles v0.14.0 // indirect @@ -99,7 +99,7 @@ require ( github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-git/v5 v5.15.0 // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -218,11 +218,11 @@ require ( go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/sdk v1.34.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect diff --git a/cliv2/go.sum b/cliv2/go.sum index 441c180634..d51bc2f642 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -691,8 +691,8 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= -github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc= -github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= +github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -880,8 +880,8 @@ github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3I github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= @@ -1444,22 +1444,22 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= diff --git a/cliv2/internal/feature-flag-gateway/model.go b/cliv2/internal/feature-flag-gateway/model.go new file mode 100644 index 0000000000..3e9522c4ef --- /dev/null +++ b/cliv2/internal/feature-flag-gateway/model.go @@ -0,0 +1,42 @@ +package featureflaggateway + +// FeatureFlagRequest request body for feature flag request. +type FeatureFlagRequest struct { + Data FeatureFlagsData `json:"data"` +} + +// FeatureFlagsData represents feature flags response data. +type FeatureFlagsData struct { + Type string `json:"type"` + Attributes FeatureFlagsRequestAttributes `json:"attributes"` +} + +// FeatureFlagsRequestAttributes represents feature flags request attributes. +type FeatureFlagsRequestAttributes struct { + Flags []string `json:"flags"` +} + +// FeatureFlagsResponse represents the updated response format. +type FeatureFlagsResponse struct { + Data FeatureFlagsDataItem `json:"data"` +} + +// FeatureFlagsDataItem represents the "data" object (previously slice). +type FeatureFlagsDataItem struct { + ID string `json:"id"` + Type string `json:"type"` + Attributes FeatureFlagAttributesList `json:"attributes"` +} + +// FeatureFlagAttributesList represents attributes containing evaluations + evaluated_at. +type FeatureFlagAttributesList struct { + Evaluations []FeatureFlagAttributes `json:"evaluations"` + EvaluatedAt string `json:"evaluatedAt"` +} + +// FeatureFlagAttributes represent one evaluation result. +type FeatureFlagAttributes struct { + Key string `json:"key"` + Value bool `json:"value"` + Reason string `json:"reason"` +} diff --git a/cliv2/internal/feature-flag-gateway/service.go b/cliv2/internal/feature-flag-gateway/service.go new file mode 100644 index 0000000000..b8bb59db07 --- /dev/null +++ b/cliv2/internal/feature-flag-gateway/service.go @@ -0,0 +1,137 @@ +package featureflaggateway + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "path" + "strings" + "time" +) + +const ( + // ApplicationJSON is the content type for JSON. + ApplicationJSON = "application/json" + userAgent = "User-Agent" + contentType = "Content-Type" + accept = "Accept" + snykCli = "snyk-cli" + authorization = "Authorization" + defaultVersion = "2024-10-15" +) + +// Service is a service for interacting with Feature Flag Gateway. +type Service interface { + EvaluateFlags(ctx context.Context, flags []string, orgID string, version string, token string) (*FeatureFlagsResponse, error) +} + +// Client is an HTTP client for the Feature Flag Gateway +// endpoint. +type Client struct { + baseURL *url.URL + client *http.Client +} + +// NewService creates a new Feature Flag Gateway client. +func NewService(baseURL *url.URL, requestTimeout time.Duration) (*Client, error) { + if baseURL == nil { + return nil, fmt.Errorf("base URL is nil") + } + + client := &http.Client{ + Timeout: requestTimeout, + } + + return &Client{ + baseURL: baseURL, + client: client, + }, nil +} + +// EvaluateFlags evaluates feature flags for a given organization. +func (c *Client) EvaluateFlags(ctx context.Context, flags []string, orgID string, version string, token string) (featureFlagsResponse *FeatureFlagsResponse, retErr error) { + orgID = strings.TrimSpace(orgID) + if orgID == "" { + return nil, fmt.Errorf("orgID is required") + } + + token = strings.TrimSpace(token) + if token == "" { + return nil, fmt.Errorf("token is required") + } + + if version == "" { + version = defaultVersion + } + + reqBody := buildRequest(flags) + bodyBytes, err := json.Marshal(reqBody) + if err != nil { + return featureFlagsResponse, fmt.Errorf("marshal evaluate flags request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.getBaseURL(orgID, version), bytes.NewReader(bodyBytes)) + if err != nil { + return featureFlagsResponse, fmt.Errorf("create evaluate flagsrequest: %w", err) + } + + req.Header.Add(contentType, ApplicationJSON) + req.Header.Add(accept, ApplicationJSON) + req.Header.Add(userAgent, snykCli) + req.Header.Set(authorization, "token "+token) + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("making evaluate flags request: %w", err) + } + defer func() { + retErr = errors.Join(retErr, resp.Body.Close()) + }() + + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("evaluate flags failed: status=%d", resp.StatusCode) + return nil, err + } + + if err := json.NewDecoder(resp.Body).Decode(&featureFlagsResponse); err != nil { + return nil, fmt.Errorf("decode evaluate flags response: %w", err) + } + + return featureFlagsResponse, nil +} + +func buildRequest(flags []string) FeatureFlagRequest { + return FeatureFlagRequest{ + Data: FeatureFlagsData{ + Type: "feature_flags_evaluation", + Attributes: FeatureFlagsRequestAttributes{ + Flags: flags, + }, + }, + } +} + +func (c *Client) getBaseURL(orgID, version string) string { + if version == "" { + version = defaultVersion + } + + u := *c.baseURL + u.Path = path.Join( + u.Path, + "orgs", + url.PathEscape(orgID), + "feature_flags", + "evaluation", + ) + + q := u.Query() + q.Set("version", version) + u.RawQuery = q.Encode() + + return u.String() +} diff --git a/cliv2/internal/feature-flag-gateway/service_test.go b/cliv2/internal/feature-flag-gateway/service_test.go new file mode 100644 index 0000000000..6b5f6fa489 --- /dev/null +++ b/cliv2/internal/feature-flag-gateway/service_test.go @@ -0,0 +1,166 @@ +package featureflaggateway + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" +) + +func TestEvaluateFlags_Success(t *testing.T) { + orgID := "org-123" + flags := []string{"flag-a", "flag-b"} + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Fatalf("expected method POST, got %s", r.Method) + } + + if !strings.HasSuffix(r.URL.Path, "/orgs/"+url.PathEscape(orgID)+"/feature_flags/evaluation") { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + + var req FeatureFlagRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("failed to decode request body: %v", err) + } + if len(req.Data.Attributes.Flags) != len(flags) { + t.Fatalf("expected %d flags, got %d", len(flags), len(req.Data.Attributes.Flags)) + } + + w.Header().Set("Content-Type", ApplicationJSON) + w.WriteHeader(http.StatusOK) + resp := FeatureFlagsResponse{ + Data: FeatureFlagsDataItem{ + ID: "some-id", + Type: "feature_flags_evaluation", + Attributes: FeatureFlagAttributesList{ + Evaluations: []FeatureFlagAttributes{ + {Key: "flag-a", Value: true, Reason: "default"}, + {Key: "flag-b", Value: false, Reason: "default"}, + }, + EvaluatedAt: "2024-10-15T00:00:00Z", + }, + }, + } + + if err := json.NewEncoder(w).Encode(&resp); err != nil { + t.Fatalf("failed to encode response: %v", err) + } + })) + defer ts.Close() + + baseURL, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("failed to parse URL: %v", err) + } + svc, err := NewService(baseURL, 5*time.Second) + if err != nil { + t.Fatalf("NewService error: %v", err) + } + + ctx := context.Background() + got, err := svc.EvaluateFlags(ctx, flags, orgID, "", "token") + if err != nil { + t.Fatalf("EvaluateFlags returned error: %v", err) + } + + if got == nil { + t.Fatalf("expected non-nil response") + } + + if got.Data.ID != "some-id" { + t.Fatalf("expected Data.ID %q, got %q", "some-id", got.Data.ID) + } + + if len(got.Data.Attributes.Evaluations) != 2 { + t.Fatalf("expected 2 evaluations, got %d", len(got.Data.Attributes.Evaluations)) + } + + if got.Data.Attributes.Evaluations[0].Key != "flag-a" || !got.Data.Attributes.Evaluations[0].Value { + t.Fatalf("unexpected first evaluation: %+v", got.Data.Attributes.Evaluations[0]) + } +} + +func TestEvaluateFlags_EmptyOrgID(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("server should not have been called for empty orgID") + })) + defer ts.Close() + + baseURL, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("failed to parse URL: %v", err) + } + svc, err := NewService(baseURL, 5*time.Second) + if err != nil { + t.Fatalf("NewService error: %v", err) + } + + ctx := context.Background() + _, err = svc.EvaluateFlags(ctx, []string{"flag-a"}, " ", "", "token") + if err == nil { + t.Fatalf("expected error for empty orgID, got nil") + } + + if !strings.Contains(err.Error(), "orgID is required") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestEvaluateFlags_EmptyToken(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("server should not have been called for empty token") + })) + defer ts.Close() + + baseURL, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("failed to parse URL: %v", err) + } + svc, err := NewService(baseURL, 5*time.Second) + if err != nil { + t.Fatalf("NewService error: %v", err) + } + + ctx := context.Background() + _, err = svc.EvaluateFlags(ctx, []string{"flag-a"}, "orgid", "", "") + if err == nil { + t.Fatalf("expected error for empty token, got nil") + } + + if !strings.Contains(err.Error(), "token is required") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestEvaluateFlags_Non200Status(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"error": "something went wrong"}`)) + })) + defer ts.Close() + + baseURL, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("failed to parse URL: %v", err) + } + svc, err := NewService(baseURL, 5*time.Second) + if err != nil { + t.Fatalf("NewService error: %v", err) + } + + ctx := context.Background() + _, err = svc.EvaluateFlags(ctx, []string{"flag-a"}, "org-123", "", "token") + if err == nil { + t.Fatalf("expected error for non-200 status, got nil") + } + + if !strings.Contains(err.Error(), "status=500") { + t.Fatalf("expected error to mention status=500, got: %v", err) + } +} diff --git a/package-lock.json b/package-lock.json index 5ce8e93db1..8d1e74a3c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "snyk-go-plugin": "1.28.0", "snyk-gradle-plugin": "5.1.1", "snyk-module": "3.1.0", - "snyk-mvn-plugin": "4.3.3", + "snyk-mvn-plugin": "4.5.0", "snyk-nodejs-lockfile-parser": "2.4.3", "snyk-nodejs-plugin": "1.4.5", "snyk-nuget-plugin": "2.12.0", @@ -286,6 +286,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -2446,6 +2447,7 @@ "version": "3.5.1", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", + "peer": true, "dependencies": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", @@ -3071,10 +3073,11 @@ } }, "node_modules/@snyk/dep-graph": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-2.10.0.tgz", - "integrity": "sha512-Gx4YbGPf+jIlARMBfmVxPH5nZuMqXVe3W17y0S2uPsVRCt9NBglsEEGpjIFppq0r6eM0eOV6Iergh0NdmchTnA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-2.12.1.tgz", + "integrity": "sha512-pPa/l4BTrL7q5YUBVcgsRDNJJxiz8TEguzniHoGX3xHzZl7kTFWMWC2dx3jtpFUrY/JrwZ8KlfGg48EYMi8FdA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "event-loop-spinner": "^2.1.0", "lodash.clone": "^4.5.0", @@ -3847,6 +3850,7 @@ "resolved": "https://registry.npmjs.org/@tapjs/core/-/core-2.0.1.tgz", "integrity": "sha512-q+8d+ohw5kudktIqgP5ETBcPWAPip+kMIxs2eL2G3dV+7Gc8WrH43cCPrbSGPRITIOSIDPrtpQZEcZwQNqDdQw==", "dev": true, + "peer": true, "dependencies": { "@tapjs/processinfo": "^3.1.7", "@tapjs/stack": "2.0.1", @@ -5117,7 +5121,8 @@ "node_modules/@types/node": { "version": "14.17.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.10.tgz", - "integrity": "sha512-09x2d6kNBwjHgyh3jOUE2GE4DFoxDriDvWdu6mFhMP1ysynGYazt4ecZmJlL6/fe4Zi2vtYvTvtL7epjQQrBhA==" + "integrity": "sha512-09x2d6kNBwjHgyh3jOUE2GE4DFoxDriDvWdu6mFhMP1ysynGYazt4ecZmJlL6/fe4Zi2vtYvTvtL7epjQQrBhA==", + "peer": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -5340,6 +5345,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.30.0.tgz", "integrity": "sha512-HJ0XuluSZSxeboLU7Q2VQ6eLlCwXPBOGnA7CqgBnz2Db3JRQYyBDJgQnop6TZ+rsbSx5gEdWhw4rE4mDa1FnZg==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "4.30.0", "@typescript-eslint/types": "4.30.0", @@ -6028,6 +6034,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7033,6 +7040,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -9603,6 +9611,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", "ajv": "^6.10.0", @@ -12978,6 +12987,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -15548,6 +15558,7 @@ "resolved": "https://registry.npmjs.org/ky/-/ky-0.12.0.tgz", "integrity": "sha512-t9b7v3V2fGwAcQnnDDQwKQGF55eWrf4pwi1RN08Fy8b/9GEwV7Ea0xQiaSW6ZbeghBHIwl8kgnla4vVo9seepQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -18092,6 +18103,7 @@ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz", "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==", "dev": true, + "peer": true, "dependencies": { "colorette": "^1.2.2", "nanoid": "^3.1.23", @@ -18668,6 +18680,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -19601,6 +19614,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -20566,6 +20580,7 @@ "version": "1.31.0", "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-1.31.0.tgz", "integrity": "sha512-nGSua40dcI/ISDDW46EYSjwVZxdWohb4bDlHFYtudL5bxo0PV9wFA1QeZewKQVeHLVaGkrESXdqQubP0pFf4vA==", + "peer": true, "dependencies": { "event-loop-spinner": "^2.1.0", "lodash.clone": "^4.5.0", @@ -20762,13 +20777,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/snyk-mvn-plugin": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/snyk-mvn-plugin/-/snyk-mvn-plugin-4.3.3.tgz", - "integrity": "sha512-CzgiMRNdeAkX5HZAzqdzDA3/1twkCIM2NVvKNo26QEt6G2X/BQuG0/Lg2R2sNu04BbRDILl73rfQ1u9wJT6k7g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/snyk-mvn-plugin/-/snyk-mvn-plugin-4.5.0.tgz", + "integrity": "sha512-WTZ/SgmopXRNadSNJfFAE+C6xoIRRfW8iAbt+P5v6EyYFwC0trhiebqcIAB6kpjSjfh88GlXTd9qOwHFcekRHw==", + "license": "Apache-2.0", "dependencies": { "@common.js/yocto-queue": "^1.1.1", "@snyk/cli-interface": "2.14.1", - "@snyk/dep-graph": "^2.9.0", + "@snyk/dep-graph": "^2.12.0", "debug": "^4.3.4", "glob": "^7.1.6", "packageurl-js": "^2.0.1", @@ -22642,6 +22658,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", "dev": true, + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -22986,6 +23003,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -23303,6 +23321,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -23592,6 +23611,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -24041,6 +24061,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.54.0.tgz", "integrity": "sha512-MAVKJMsIUotOQKzFOmN8ZkmMlj7BOyjDU6t1lomW9dWOme5WTStzGa3HMLdV1KYD1AiFETGsznL4LMSvj4tukw==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.0", "@types/estree": "^0.0.50", @@ -24088,6 +24109,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.8.0.tgz", "integrity": "sha512-+iBSWsX16uVna5aAYN6/wjhJy1q/GKk4KjKvfg90/6hykCTSgozbfz5iRgDTSJt/LgSbYxdBX3KBHeobIs+ZEw==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.0.4", @@ -24304,6 +24326,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -24943,6 +24966,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -26528,6 +26552,7 @@ "version": "3.5.1", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz", "integrity": "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==", + "peer": true, "requires": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", @@ -27113,9 +27138,10 @@ } }, "@snyk/dep-graph": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-2.10.0.tgz", - "integrity": "sha512-Gx4YbGPf+jIlARMBfmVxPH5nZuMqXVe3W17y0S2uPsVRCt9NBglsEEGpjIFppq0r6eM0eOV6Iergh0NdmchTnA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-2.12.1.tgz", + "integrity": "sha512-pPa/l4BTrL7q5YUBVcgsRDNJJxiz8TEguzniHoGX3xHzZl7kTFWMWC2dx3jtpFUrY/JrwZ8KlfGg48EYMi8FdA==", + "peer": true, "requires": { "event-loop-spinner": "^2.1.0", "lodash.clone": "^4.5.0", @@ -27785,6 +27811,7 @@ "resolved": "https://registry.npmjs.org/@tapjs/core/-/core-2.0.1.tgz", "integrity": "sha512-q+8d+ohw5kudktIqgP5ETBcPWAPip+kMIxs2eL2G3dV+7Gc8WrH43cCPrbSGPRITIOSIDPrtpQZEcZwQNqDdQw==", "dev": true, + "peer": true, "requires": { "@tapjs/processinfo": "^3.1.7", "@tapjs/stack": "2.0.1", @@ -28733,7 +28760,8 @@ "@types/node": { "version": "14.17.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.10.tgz", - "integrity": "sha512-09x2d6kNBwjHgyh3jOUE2GE4DFoxDriDvWdu6mFhMP1ysynGYazt4ecZmJlL6/fe4Zi2vtYvTvtL7epjQQrBhA==" + "integrity": "sha512-09x2d6kNBwjHgyh3jOUE2GE4DFoxDriDvWdu6mFhMP1ysynGYazt4ecZmJlL6/fe4Zi2vtYvTvtL7epjQQrBhA==", + "peer": true }, "@types/normalize-package-data": { "version": "2.4.1", @@ -28907,6 +28935,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.30.0.tgz", "integrity": "sha512-HJ0XuluSZSxeboLU7Q2VQ6eLlCwXPBOGnA7CqgBnz2Db3JRQYyBDJgQnop6TZ+rsbSx5gEdWhw4rE4mDa1FnZg==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "4.30.0", "@typescript-eslint/types": "4.30.0", @@ -29443,7 +29472,8 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -30156,6 +30186,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -32052,6 +32083,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", "dev": true, + "peer": true, "requires": { "@babel/code-frame": "^7.0.0", "ajv": "^6.10.0", @@ -34486,6 +34518,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "requires": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -36354,7 +36387,8 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/ky/-/ky-0.12.0.tgz", "integrity": "sha512-t9b7v3V2fGwAcQnnDDQwKQGF55eWrf4pwi1RN08Fy8b/9GEwV7Ea0xQiaSW6ZbeghBHIwl8kgnla4vVo9seepQ==", - "dev": true + "dev": true, + "peer": true }, "ky-universal": { "version": "0.3.0", @@ -37000,8 +37034,7 @@ "dev": true }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" @@ -38289,6 +38322,7 @@ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz", "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==", "dev": true, + "peer": true, "requires": { "colorette": "^1.2.2", "nanoid": "^3.1.23", @@ -38710,6 +38744,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, + "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -39378,6 +39413,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -40108,6 +40144,7 @@ "version": "1.31.0", "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-1.31.0.tgz", "integrity": "sha512-nGSua40dcI/ISDDW46EYSjwVZxdWohb4bDlHFYtudL5bxo0PV9wFA1QeZewKQVeHLVaGkrESXdqQubP0pFf4vA==", + "peer": true, "requires": { "event-loop-spinner": "^2.1.0", "lodash.clone": "^4.5.0", @@ -40262,13 +40299,13 @@ } }, "snyk-mvn-plugin": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/snyk-mvn-plugin/-/snyk-mvn-plugin-4.3.3.tgz", - "integrity": "sha512-CzgiMRNdeAkX5HZAzqdzDA3/1twkCIM2NVvKNo26QEt6G2X/BQuG0/Lg2R2sNu04BbRDILl73rfQ1u9wJT6k7g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/snyk-mvn-plugin/-/snyk-mvn-plugin-4.5.0.tgz", + "integrity": "sha512-WTZ/SgmopXRNadSNJfFAE+C6xoIRRfW8iAbt+P5v6EyYFwC0trhiebqcIAB6kpjSjfh88GlXTd9qOwHFcekRHw==", "requires": { "@common.js/yocto-queue": "^1.1.1", "@snyk/cli-interface": "2.14.1", - "@snyk/dep-graph": "^2.9.0", + "@snyk/dep-graph": "^2.12.0", "debug": "^4.3.4", "glob": "^7.1.6", "packageurl-js": "^2.0.1", @@ -41700,7 +41737,8 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", - "dev": true + "dev": true, + "peer": true }, "yaml-types": { "version": "0.3.0", @@ -41954,7 +41992,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true + "dev": true, + "peer": true } } }, @@ -42165,6 +42204,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -42351,7 +42391,8 @@ "typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "peer": true }, "uglify-js": { "version": "3.17.4", @@ -42692,6 +42733,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.54.0.tgz", "integrity": "sha512-MAVKJMsIUotOQKzFOmN8ZkmMlj7BOyjDU6t1lomW9dWOme5WTStzGa3HMLdV1KYD1AiFETGsznL4LMSvj4tukw==", "dev": true, + "peer": true, "requires": { "@types/eslint-scope": "^3.7.0", "@types/estree": "^0.0.50", @@ -42729,7 +42771,8 @@ "version": "8.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", - "dev": true + "dev": true, + "peer": true }, "acorn-import-assertions": { "version": "1.7.6", @@ -42745,6 +42788,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.8.0.tgz", "integrity": "sha512-+iBSWsX16uVna5aAYN6/wjhJy1q/GKk4KjKvfg90/6hykCTSgozbfz5iRgDTSJt/LgSbYxdBX3KBHeobIs+ZEw==", "dev": true, + "peer": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.0.4", diff --git a/package.json b/package.json index 176f34a7b9..38c5767634 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "snyk-go-plugin": "1.28.0", "snyk-gradle-plugin": "5.1.1", "snyk-module": "3.1.0", - "snyk-mvn-plugin": "4.3.3", + "snyk-mvn-plugin": "4.5.0", "snyk-nodejs-lockfile-parser": "2.4.3", "snyk-nodejs-plugin": "1.4.5", "snyk-nuget-plugin": "2.12.0", diff --git a/src/cli/commands/fix/validate-fix-command-is-supported.ts b/src/cli/commands/fix/validate-fix-command-is-supported.ts index 665210472a..19b283e7ac 100644 --- a/src/cli/commands/fix/validate-fix-command-is-supported.ts +++ b/src/cli/commands/fix/validate-fix-command-is-supported.ts @@ -2,11 +2,12 @@ import * as Debug from 'debug'; import { getEcosystemForTest } from '../../../lib/ecosystems'; -import { isFeatureFlagSupportedForOrg } from '../../../lib/feature-flags'; import { FeatureNotSupportedByEcosystemError } from '../../../lib/errors/not-supported-by-ecosystem'; import { Options, TestOptions } from '../../../lib/types'; import { AuthFailedError } from '../../../lib/errors'; import chalk from 'chalk'; +import { getEnabledFeatureFlags } from '../../../lib/feature-flag-gateway'; +import { getOrganizationID } from '../../../lib/organization'; const debug = Debug('snyk-fix'); const snykFixFeatureFlag = 'cliSnykFix'; @@ -23,23 +24,24 @@ export async function validateFixCommandIsSupported( throw new FeatureNotSupportedByEcosystemError('snyk fix', ecosystem); } - const snykFixSupported = await isFeatureFlagSupportedForOrg( - snykFixFeatureFlag, - options.org, + //Batch fetch of feature flags to reduce latency + const orgID = getOrganizationID(); + const featureFlags = await getEnabledFeatureFlags( + [snykFixFeatureFlag], + orgID, ); + const snykFixSupported = featureFlags.has(snykFixFeatureFlag); debug('Feature flag check returned: ', snykFixSupported); - if (snykFixSupported.code === 401 || snykFixSupported.code === 403) { - throw AuthFailedError(snykFixSupported.error, snykFixSupported.code); + if (!snykFixSupported) { + throw AuthFailedError('snykFixSupported is false', 403); } - if (!snykFixSupported.ok) { + if (!snykFixSupported) { const snykFixErrorMessage = chalk.red( - `\`snyk fix\` is not supported${ - options.org ? ` for org '${options.org}'` : '' - }.`, + `\`snyk fix\` is not supported${orgID ? ` for org '${orgID}'` : ''}.`, ) + '\nSee documentation on how to enable this beta feature: https://docs.snyk.io/snyk-cli/fix-vulnerabilities-from-the-cli/automatic-remediation-with-snyk-fix#enabling-snyk-fix'; const unsupportedError = new Error(snykFixErrorMessage); diff --git a/src/cli/commands/monitor/index.ts b/src/cli/commands/monitor/index.ts index 0b8c63be81..08fc83567a 100644 --- a/src/cli/commands/monitor/index.ts +++ b/src/cli/commands/monitor/index.ts @@ -48,10 +48,6 @@ import { isMultiProjectScan } from '../../../lib/is-multi-project-scan'; import { getEcosystem, monitorEcosystem } from '../../../lib/ecosystems'; import { getFormattedMonitorOutput } from '../../../lib/ecosystems/monitor'; import { processCommandArgs } from '../process-command-args'; -import { - hasFeatureFlag, - hasFeatureFlagOrDefault, -} from '../../../lib/feature-flags'; import { SCAN_USR_LIB_JARS_FEATURE_FLAG, CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, @@ -65,6 +61,8 @@ import { MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, } from '../../../lib/package-managers'; import { normalizeTargetFile } from '../../../lib/normalize-target-file'; +import { getOrganizationID } from '../../../lib/organization'; +import { getEnabledFeatureFlags } from '../../../lib/feature-flag-gateway'; const SEPARATOR = '\n-------------------------------------------------------\n'; const debug = Debug('snyk'); @@ -102,6 +100,18 @@ export default async function monitor(...args0: MethodArgs): Promise { ); } + //Batch fetch of feature flags to reduce latency + const batchFeatureFlags = await getEnabledFeatureFlags( + [ + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + SCAN_USR_LIB_JARS_FEATURE_FLAG, + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + ], + getOrganizationID(), + ); + if (!options.docker) { checkOSSPaths(paths, options); } @@ -119,11 +129,9 @@ export default async function monitor(...args0: MethodArgs): Promise { } else if (options[APP_VULNS_OPTION]) { options[EXCLUDE_APP_VULNS_OPTION] = false; } else { - options[EXCLUDE_APP_VULNS_OPTION] = !(await hasFeatureFlagOrDefault( + options[EXCLUDE_APP_VULNS_OPTION] = !batchFeatureFlags.has( CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, - options, - false, - )); + ); // we can't print the warning message with JSON output as that would make // the JSON output invalid. @@ -139,12 +147,7 @@ export default async function monitor(...args0: MethodArgs): Promise { } // Check scanUsrLibJars feature flag and add --include-system-jars parameter - const scanUsrLibJarsEnabled = await hasFeatureFlagOrDefault( - SCAN_USR_LIB_JARS_FEATURE_FLAG, - options, - false, - ); - if (scanUsrLibJarsEnabled) { + if (batchFeatureFlags.has(SCAN_USR_LIB_JARS_FEATURE_FLAG)) { options[INCLUDE_SYSTEM_JARS_OPTION] = true; } } @@ -185,52 +188,27 @@ export default async function monitor(...args0: MethodArgs): Promise { ); } - let hasPnpmSupport = false; - let hasImprovedDotnetWithoutPublish = false; - let enableMavenDverboseExhaustiveDeps = false; - try { - hasPnpmSupport = (await hasFeatureFlag( - PNPM_FEATURE_FLAG, - options, - )) as boolean; - if (options['dotnet-runtime-resolution']) { - hasImprovedDotnetWithoutPublish = (await hasFeatureFlag( - DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, - options, - )) as boolean; + if (options['dotnet-runtime-resolution']) { + if (batchFeatureFlags.has(DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG)) { + options.useImprovedDotnetWithoutPublish = true; } - } catch (err) { - hasPnpmSupport = false; } - try { - const args = options['_doubleDashArgs'] || []; - const verboseEnabled = - args.includes('-Dverbose') || - args.includes('-Dverbose=true') || - !!options['print-graph']; - if (verboseEnabled) { - enableMavenDverboseExhaustiveDeps = (await hasFeatureFlag( - MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, - options, - )) as boolean; - if (enableMavenDverboseExhaustiveDeps) { - options.mavenVerboseIncludeAllVersions = - enableMavenDverboseExhaustiveDeps; - } + const args = options['_doubleDashArgs'] || []; + const verboseEnabled = + args.includes('-Dverbose') || + args.includes('-Dverbose=true') || + !!options['print-graph']; + if (verboseEnabled) { + if (batchFeatureFlags.has(MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF)) { + options.mavenVerboseIncludeAllVersions = true; } - } catch (err) { - enableMavenDverboseExhaustiveDeps = false; } - const featureFlags = hasPnpmSupport + const featureFlags = batchFeatureFlags.has(PNPM_FEATURE_FLAG) ? new Set([PNPM_FEATURE_FLAG]) : new Set(); - if (hasImprovedDotnetWithoutPublish) { - options.useImprovedDotnetWithoutPublish = true; - } - // Part 1: every argument is a scan target; process them sequentially for (const path of paths) { debug(`Processing ${path}...`); diff --git a/src/cli/commands/test/iac/index.ts b/src/cli/commands/test/iac/index.ts index 31b16f5e06..8520281f5e 100644 --- a/src/cli/commands/test/iac/index.ts +++ b/src/cli/commands/test/iac/index.ts @@ -6,7 +6,6 @@ import { validateTestOptions } from '../validate-test-options'; import { setDefaultTestOptions } from '../set-default-test-options'; import { processCommandArgs } from '../../process-command-args'; -import { hasFeatureFlag } from '../../../../lib/feature-flags'; import { buildDefaultOciRegistry } from './local-execution/rules/rules'; import { getIacOrgSettings } from './local-execution/measurable-methods'; import config from '../../../../lib/config'; @@ -15,6 +14,8 @@ import { scan } from './scan'; import { buildOutput, buildSpinner, printHeader } from './output'; import { InvalidArgumentError } from './local-execution/assert-iac-options-flag'; import { IaCTestFlags } from './local-execution/types'; +import { getOrganizationID } from '../../../../lib/organization'; +import { getEnabledFeatureFlags } from '../../../../lib/feature-flag-gateway'; export default async function ( ...args: MethodArgs @@ -36,8 +37,14 @@ export default async function ( const buildOciRegistry = () => buildDefaultOciRegistry(iacOrgSettings); - const isIacShareCliResultsCustomRulesSupported = Boolean( - await hasFeatureFlag('iacShareCliResultsCustomRules', options), + //Batch fetch of feature flags to reduce latency + const featureFlags = await getEnabledFeatureFlags( + ['iacShareCliResultsCustomRules'], + getOrganizationID(), + ); + + const isIacShareCliResultsCustomRulesSupported = featureFlags.has( + 'iacShareCliResultsCustomRules', ); const isIacCustomRulesEntitlementEnabled = Boolean( diff --git a/src/cli/commands/test/iac/local-execution/process-results/share-results.ts b/src/cli/commands/test/iac/local-execution/process-results/share-results.ts index 8e2ad9fb97..737965bc73 100644 --- a/src/cli/commands/test/iac/local-execution/process-results/share-results.ts +++ b/src/cli/commands/test/iac/local-execution/process-results/share-results.ts @@ -1,4 +1,3 @@ -import { isFeatureFlagSupportedForOrg } from '../../../../../../lib/feature-flags'; import { shareResults } from './cli-share-results'; import { Policy } from 'snyk-policy'; import { @@ -9,6 +8,8 @@ import { import { FeatureFlagError } from '../assert-iac-options-flag'; import { formatShareResults } from './share-results-formatter'; import { IacFileScanResult, IaCTestFlags, ShareResultsOutput } from '../types'; +import { getOrganizationID } from '../../../../../../lib/organization'; +import { getEnabledFeatureFlags } from '../../../../../../lib/feature-flag-gateway'; export async function formatAndShareResults({ results, @@ -29,11 +30,12 @@ export async function formatAndShareResults({ projectRoot: string; meta: IacOutputMeta; }): Promise { - const isCliReportEnabled = await isFeatureFlagSupportedForOrg( - 'iacCliShareResults', - orgPublicId, + const featureFlags = await getEnabledFeatureFlags( + ['iacCliShareResults'], + orgPublicId?.trim() || getOrganizationID(), ); - if (!isCliReportEnabled.ok) { + + if (!featureFlags.has('iacCliShareResults')) { throw new FeatureFlagError('report', 'iacCliShareResults'); } diff --git a/src/cli/commands/test/iac/local-execution/rules/rules.ts b/src/cli/commands/test/iac/local-execution/rules/rules.ts index e6b0b01fb7..4a72e44853 100644 --- a/src/cli/commands/test/iac/local-execution/rules/rules.ts +++ b/src/cli/commands/test/iac/local-execution/rules/rules.ts @@ -24,8 +24,9 @@ import { } from '../../../../../../lib/formatters/iac-output/text'; import { OciRegistry, RemoteOciRegistry } from './oci-registry'; import { isValidUrl } from '../url-utils'; -import { isFeatureFlagSupportedForOrg } from '../../../../../../lib/feature-flags'; import { CLI } from '@snyk/error-catalog-nodejs-public'; +import { getOrganizationID } from '../../../../../../lib/organization'; +import { getEnabledFeatureFlags } from '../../../../../../lib/feature-flag-gateway'; export async function initRules( buildOciRegistry: () => OciRegistry, @@ -56,12 +57,12 @@ export async function initRules( let userMessage = `${customRulesMessage}${EOL}`; if (options.report) { - const isCliReportCustomRulesEnabled = await isFeatureFlagSupportedForOrg( - 'iacShareCliResultsCustomRules', - orgPublicId, + const featureFlags = await getEnabledFeatureFlags( + ['iacShareCliResultsCustomRules'], + orgPublicId?.trim() || getOrganizationID(), ); - if (!isCliReportCustomRulesEnabled.ok) { + if (!featureFlags.has('iacShareCliResultsCustomRules')) { userMessage += `${customRulesReportMessage}${EOL}`; } } diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index f8396252a6..100b6e74bc 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -44,10 +44,6 @@ import { } from '../../../lib/spotlight-vuln-notification'; import iacTestCommand from './iac'; import * as iacTestCommandV2 from './iac/v2'; -import { - hasFeatureFlag, - hasFeatureFlagOrDefault, -} from '../../../lib/feature-flags'; import { SCAN_USR_LIB_JARS_FEATURE_FLAG, CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, @@ -56,6 +52,8 @@ import { APP_VULNS_OPTION, } from '../constants'; import { checkOSSPaths } from '../../../lib/check-paths'; +import { getOrganizationID } from '../../../lib/organization'; +import { getEnabledFeatureFlags } from '../../../lib/feature-flag-gateway'; const debug = Debug('snyk-test'); const SEPARATOR = '\n-------------------------------------------------------\n'; @@ -75,15 +73,27 @@ export default async function test( const options = setDefaultTestOptions(originalOptions); - if (originalOptions.iac) { - const iacNewEngine = await hasFeatureFlag('iacNewEngine', options); - const iacIntegratedExperience = await hasFeatureFlag( + const featureFlags = await getEnabledFeatureFlags( + [ + 'iacNewEngine', 'iacIntegratedExperience', - options, - ); + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + SCAN_USR_LIB_JARS_FEATURE_FLAG, + ], + getOrganizationID(), + ); + + if (originalOptions.iac) { // temporary placeholder for the "new" flow that integrates with UPE - if (iacIntegratedExperience || iacNewEngine) { - return await iacTestCommandV2.test(paths, originalOptions, iacNewEngine); + if ( + featureFlags.has('iacIntegratedExperience') || + featureFlags.has('iacNewEngine') + ) { + return await iacTestCommandV2.test( + paths, + originalOptions, + featureFlags.has('iacNewEngine'), + ); } else { return await iacTestCommand(...args); } @@ -120,11 +130,9 @@ export default async function test( } else if (options[APP_VULNS_OPTION]) { options[EXCLUDE_APP_VULNS_OPTION] = false; } else { - options[EXCLUDE_APP_VULNS_OPTION] = !(await hasFeatureFlagOrDefault( + options[EXCLUDE_APP_VULNS_OPTION] = !featureFlags.has( CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, - options, - true, - )); + ); // we can't print the warning message with JSON output as that would make // the JSON output invalid. @@ -140,10 +148,8 @@ export default async function test( } // Check scanUsrLibJars feature flag and add --include-system-jars parameter - const scanUsrLibJarsEnabled = await hasFeatureFlagOrDefault( + const scanUsrLibJarsEnabled = featureFlags.has( SCAN_USR_LIB_JARS_FEATURE_FLAG, - options, - false, ); if (scanUsrLibJarsEnabled) { options[INCLUDE_SYSTEM_JARS_OPTION] = true; diff --git a/src/lib/feature-flag-gateway/index.ts b/src/lib/feature-flag-gateway/index.ts new file mode 100644 index 0000000000..9a12ad6f03 --- /dev/null +++ b/src/lib/feature-flag-gateway/index.ts @@ -0,0 +1,80 @@ +import config from '../config'; +import { getAuthHeader } from '../api-token'; +import { FeatureFlagsEvaluationResponse } from './types'; +import { makeRequest } from '../request'; +import { AuthFailedError, ValidationError } from '../errors'; +import { TestLimitReachedError } from '../../cli/commands/test/iac/local-execution/usage-tracking'; + +export const SHOW_MAVEN_BUILD_SCOPE = 'show-maven-build-scope'; + +export async function evaluateFeatureFlagsForOrg( + flags: string[], + orgId: string, + version = '2024-10-15', +): Promise { + if (!orgId.trim()) { + throw new Error('orgId is required'); + } + + const url = + `${config.API_HIDDEN_URL}/orgs/${encodeURIComponent(orgId)}` + + `/feature_flags/evaluation?version=${encodeURIComponent(version)}`; + + const payload = { + data: { + type: 'feature_flags_evaluation', + attributes: { flags }, + }, + }; + + const { res, body } = await makeRequest({ + url, + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.api+json', + Authorization: getAuthHeader(), + }, + body: payload, + json: true, + noCompression: true, + }); + + switch (res.statusCode) { + case 200: + break; + case 401: + throw AuthFailedError(); + case 429: + throw new TestLimitReachedError(); + default: + throw new ValidationError( + res.body?.error ?? 'An error occurred, please contact Snyk support', + ); + } + + if (body?.errors?.length) { + const first = body.errors[0]; + throw new Error( + `Feature flag evaluation failed: ${first?.status ?? res.statusCode} ${ + first?.detail ?? res.statusMessage + }`, + ); + } + + return body as FeatureFlagsEvaluationResponse; +} + +export async function getEnabledFeatureFlags( + flags: string[], + orgId: string, +): Promise> { + try { + const result = await evaluateFeatureFlagsForOrg(flags, orgId); + const evaluations = result?.data?.attributes?.evaluations ?? []; + return new Set( + evaluations.filter((e) => e.value === true).map((e) => e.key), + ); + } catch { + return new Set(); + } +} diff --git a/src/lib/feature-flag-gateway/types.ts b/src/lib/feature-flag-gateway/types.ts new file mode 100644 index 0000000000..20bc34348e --- /dev/null +++ b/src/lib/feature-flag-gateway/types.ts @@ -0,0 +1,15 @@ +export type FeatureFlagsEvaluationResponse = { + jsonapi?: { version: string }; + data: { + id: string; + type: 'feature_flags_evaluation'; + attributes: { + evaluations: Array<{ + key: string; + value: boolean; + reason: string; + }>; + evaluatedAt: string; + }; + }; +}; diff --git a/src/lib/feature-flags/fetchFeatureFlag.ts b/src/lib/feature-flags/fetchFeatureFlag.ts deleted file mode 100644 index 73c5a410cc..0000000000 --- a/src/lib/feature-flags/fetchFeatureFlag.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { makeRequest } from '../request'; -import { getAuthHeader } from '../api-token'; -import config from '../config'; -import { assembleQueryString } from '../snyk-test/common'; -import { OrgFeatureFlagResponse } from './types'; - -export async function fetchFeatureFlag( - featureFlag: string, - org, -): Promise { - const response = await makeRequest({ - method: 'GET', - headers: { - Authorization: getAuthHeader(), - }, - qs: assembleQueryString({ org }), - url: `${config.API}/cli-config/feature-flags/${featureFlag}`, - gzip: true, - json: true, - }); - - return (response as any).body; -} diff --git a/src/lib/feature-flags/index.ts b/src/lib/feature-flags/index.ts deleted file mode 100644 index bad8339e9a..0000000000 --- a/src/lib/feature-flags/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { makeRequest } from '../request'; -import { getAuthHeader } from '../api-token'; -import config from '../config'; -import { assembleQueryString } from '../snyk-test/common'; -import { OrgFeatureFlagResponse } from './types'; -import { Options } from '../types'; -import { AuthFailedError } from '../errors'; -import * as Debug from 'debug'; - -const debug = Debug('snyk-feature-flags'); - -export async function isFeatureFlagSupportedForOrg( - featureFlag: string, - org, -): Promise { - const response = await makeRequest({ - method: 'GET', - headers: { - Authorization: getAuthHeader(), - }, - qs: assembleQueryString({ org }), - url: `${config.API}/cli-config/feature-flags/${featureFlag}`, - gzip: true, - json: true, - }); - - return (response as any).body; -} - -export async function hasFeatureFlag( - featureFlag: string, - options: Options, -): Promise { - const { code, error, ok } = await isFeatureFlagSupportedForOrg( - featureFlag, - options.org, - ); - - if (code === 401 || code === 403) { - throw AuthFailedError(error, code); - } - return ok; -} - -export async function hasFeatureFlagOrDefault( - featureFlag: string, - options: Options, - defaultValue = false, -): Promise { - try { - const result = await hasFeatureFlag(featureFlag, options); - return result ?? defaultValue; - } catch (err) { - debug(`error checking feature flag '${featureFlag}':`, err); - return defaultValue; - } -} diff --git a/src/lib/feature-flags/types.ts b/src/lib/feature-flags/types.ts deleted file mode 100644 index e9aa1134de..0000000000 --- a/src/lib/feature-flags/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type OrgFeatureFlagResponse = { - ok?: boolean; - userMessage?: string; - code?: number; - error?: string; -}; diff --git a/src/lib/organization.ts b/src/lib/organization.ts index 4fa3f4e385..408d7f6923 100644 --- a/src/lib/organization.ts +++ b/src/lib/organization.ts @@ -1,6 +1,11 @@ export function getOrganizationID(): string { - if (process.env.SNYK_INTERNAL_ORGID != undefined) { + if (process.env.SNYK_INTERNAL_ORGID) { return process.env.SNYK_INTERNAL_ORGID; } + + if (process.env.SNYK_CFG_INTERNAL_ORGID) { + return process.env.SNYK_CFG_INTERNAL_ORGID; + } + return ''; } diff --git a/src/lib/plugins/get-deps-from-plugin.ts b/src/lib/plugins/get-deps-from-plugin.ts index 13c4039a27..4873571d24 100644 --- a/src/lib/plugins/get-deps-from-plugin.ts +++ b/src/lib/plugins/get-deps-from-plugin.ts @@ -16,7 +16,7 @@ import { AUTO_DETECTABLE_FILES, detectPackageManagerFromFile, } from '../detect'; -import analytics = require('../analytics'); +import * as analytics from '../analytics'; import { convertSingleResultToMultiCustom } from './convert-single-splugin-res-to-multi-custom'; import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-multi-custom'; import { processYarnWorkspaces } from './nodejs-plugin/yarn-workspaces-parser'; @@ -102,7 +102,12 @@ export async function getDepsFromPlugin( if (!options.docker && !(options.file || options.packageManager)) { throw NoSupportedManifestsFoundError([...root]); } - const inspectRes = await getSinglePluginResult(root, options); + const inspectRes = await getSinglePluginResult( + root, + options, + '', + featureFlags, + ); if (!pluginApi.isMultiResult(inspectRes)) { if (!inspectRes.package && !inspectRes.dependencyGraph) { diff --git a/src/lib/plugins/get-multi-plugin-result.ts b/src/lib/plugins/get-multi-plugin-result.ts index 39cbaebcae..267c40cfdf 100644 --- a/src/lib/plugins/get-multi-plugin-result.ts +++ b/src/lib/plugins/get-multi-plugin-result.ts @@ -116,6 +116,7 @@ export async function getMultiPluginResult( root, optionsClone, optionsClone.file, + featureFlags, ); let resultWithScannedProjects: cliInterface.legacyPlugin.MultiProjectResult; diff --git a/src/lib/plugins/get-single-plugin-result.ts b/src/lib/plugins/get-single-plugin-result.ts index 2b69238906..df7c161825 100644 --- a/src/lib/plugins/get-single-plugin-result.ts +++ b/src/lib/plugins/get-single-plugin-result.ts @@ -1,21 +1,27 @@ -import plugins = require('.'); +import * as plugins from '.'; import { ModuleInfo } from '../module-info'; import { legacyPlugin as pluginApi } from '@snyk/cli-interface'; import { TestOptions, Options, MonitorOptions } from '../types'; import { snykHttpClient } from '../request/snyk-http-client'; import * as types from './types'; +import { SHOW_MAVEN_BUILD_SCOPE } from '../feature-flag-gateway'; export async function getSinglePluginResult( root: string, options: Options & (TestOptions | MonitorOptions), targetFile?: string, + featureFlags: Set = new Set(), ): Promise { const plugin: types.Plugin = plugins.loadPlugin(options.packageManager); const moduleInfo = ModuleInfo(plugin, options.policy); + const inspectRes: pluginApi.InspectResult = await moduleInfo.inspect( root, targetFile || options.file, - { ...options }, + { + ...options, + showMavenBuildScope: featureFlags.has(SHOW_MAVEN_BUILD_SCOPE), + }, snykHttpClient, ); return inspectRes; diff --git a/src/lib/snyk-test/index.js b/src/lib/snyk-test/index.js index 121a051b8d..625b23312c 100644 --- a/src/lib/snyk-test/index.js +++ b/src/lib/snyk-test/index.js @@ -2,16 +2,21 @@ module.exports = test; const detect = require('../detect'); const { runTest } = require('./run-test'); + const chalk = require('chalk'); const pm = require('../package-managers'); const { UnsupportedPackageManagerError } = require('../errors'); const { isMultiProjectScan } = require('../is-multi-project-scan'); -const { hasFeatureFlag } = require('../feature-flags'); const { PNPM_FEATURE_FLAG, DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, } = require('../package-managers'); +const { + getEnabledFeatureFlags, + SHOW_MAVEN_BUILD_SCOPE, +} = require('../feature-flag-gateway'); +const { getOrganizationID } = require('../organization'); async function test(root, options, callback) { if (typeof options === 'function') { @@ -33,49 +38,35 @@ async function test(root, options, callback) { } async function executeTest(root, options) { - let hasPnpmSupport = false; - let hasImprovedDotnetWithoutPublish = false; - let enableMavenDverboseExhaustiveDeps = false; - try { - hasPnpmSupport = await hasFeatureFlag(PNPM_FEATURE_FLAG, options); - if (options['dotnet-runtime-resolution']) { - hasImprovedDotnetWithoutPublish = await hasFeatureFlag( - DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, - options, - ); - if (hasImprovedDotnetWithoutPublish) { - options.useImprovedDotnetWithoutPublish = true; - } + //Batch fetch of feature flags to reduce latency + const featureFlags = await getEnabledFeatureFlags( + [ + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + SHOW_MAVEN_BUILD_SCOPE, + ], + getOrganizationID(), + ); + + if (options['dotnet-runtime-resolution']) { + if (featureFlags.has(DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG)) { + options.useImprovedDotnetWithoutPublish = true; } - } catch (err) { - hasPnpmSupport = false; } - try { - const args = options['_doubleDashArgs'] || []; - const verboseEnabled = - args.includes('-Dverbose') || - args.includes('-Dverbose=true') || - !!options['print-graph']; - if (verboseEnabled) { - enableMavenDverboseExhaustiveDeps = await hasFeatureFlag( - MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, - options, - ); - if (enableMavenDverboseExhaustiveDeps) { - options.mavenVerboseIncludeAllVersions = - enableMavenDverboseExhaustiveDeps; - } + const args = options['_doubleDashArgs'] || []; + const verboseEnabled = + args.includes('-Dverbose') || + args.includes('-Dverbose=true') || + !!options['print-graph']; + if (verboseEnabled) { + if (featureFlags.has(MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF)) { + options.mavenVerboseIncludeAllVersions = true; } - } catch (err) { - enableMavenDverboseExhaustiveDeps = false; } try { - const featureFlags = hasPnpmSupport - ? new Set([PNPM_FEATURE_FLAG]) - : new Set([]); - if (!options.allProjects) { options.packageManager = detect.detectPackageManager( root, diff --git a/test/acceptance/fake-server.ts b/test/acceptance/fake-server.ts index e519f5ba40..ccb48ee612 100644 --- a/test/acceptance/fake-server.ts +++ b/test/acceptance/fake-server.ts @@ -214,7 +214,7 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => { if ( req.url?.includes('/iac-org-settings') || - req.url?.includes('/cli-config/feature-flags/') || + req.url?.includes('/feature_flags/evaluation') || (!nextResponse && !nextStatusCode && !statusCode) ) { return next(); @@ -855,35 +855,47 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => { }); }); - app.get(basePath + '/cli-config/feature-flags/:featureFlag', (req, res) => { - const org = req.query.org; - const flag = req.params.featureFlag; - if (org === 'no-flag') { - res.send({ - ok: false, - userMessage: `Org ${org} doesn't have '${flag}' feature enabled'`, + app.post(`*hidden/orgs/:orgId/feature_flags/evaluation`, (req, res) => { + const flags: string[] = req.body?.data?.attributes?.flags ?? []; + + const { orgId } = req.params; + if (orgId === 'no-flag') { + return res.send({ + data: { + type: 'feature_flags_evaluation', + attributes: { + evaluations: [], + evaluatedAt: new Date().toISOString(), + }, + }, }); - return; } - if (featureFlags.has(flag)) { - const ffEnabled = featureFlags.get(flag); - if (ffEnabled) { - res.send({ - ok: true, - }); - } else { - res.send({ - ok: false, - userMessage: `Org ${org} doesn't have '${flag}' feature enabled'`, - }); + const evaluations = flags.map((flag) => { + if (!featureFlags.has(flag)) { + return { + key: flag, + value: false, + reason: 'not_available', + }; } - return; - } - // default: return true for all feature flags + const enabled = featureFlags.get(flag); + return { + key: flag, + value: Boolean(enabled), + reason: enabled ? 'found' : 'not_available', + }; + }); + res.send({ - ok: true, + data: { + type: 'feature_flags_evaluation', + attributes: { + evaluations, + evaluatedAt: new Date().toISOString(), + }, + }, }); }); diff --git a/test/jest/acceptance/analytics.spec.ts b/test/jest/acceptance/analytics.spec.ts index 912256e179..7661c593d7 100644 --- a/test/jest/acceptance/analytics.spec.ts +++ b/test/jest/acceptance/analytics.spec.ts @@ -25,6 +25,8 @@ describe('analytics module', () => { SNYK_INTEGRATION_NAME: 'JENKINS', SNYK_INTEGRATION_VERSION: '1.2.3', SNYK_HTTP_PROTOCOL_UPGRADE: '0', + SNYK_CFG_INTERNAL_ORGID: 'orgid-test-cli', + SNYK_API_HIDDEN_URL: 'http://localhost:' + port + '/hidden', }; server = fakeServer(baseApi, env.SNYK_TOKEN); server.listen(port, () => { diff --git a/test/jest/acceptance/snyk-container/container.spec.ts b/test/jest/acceptance/snyk-container/container.spec.ts index b73f3be37d..a7f9e49109 100644 --- a/test/jest/acceptance/snyk-container/container.spec.ts +++ b/test/jest/acceptance/snyk-container/container.spec.ts @@ -480,6 +480,7 @@ DepGraph end`, SNYK_API: 'http://localhost:' + port + baseApi, SNYK_TOKEN: '123456789', SNYK_DISABLE_ANALYTICS: '1', + SNYK_INTERNAL_ORGID: 'orgid-container-cli', }; server = fakeServer(baseApi, env.SNYK_TOKEN); server.listen(port, () => { diff --git a/test/jest/acceptance/snyk-sbom-monitor/all-projects.spec.ts b/test/jest/acceptance/snyk-sbom-monitor/all-projects.spec.ts index 92f70678b8..8f9aec7d93 100644 --- a/test/jest/acceptance/snyk-sbom-monitor/all-projects.spec.ts +++ b/test/jest/acceptance/snyk-sbom-monitor/all-projects.spec.ts @@ -21,6 +21,7 @@ describe('snyk sbom monitor (mocked server only)', () => { SNYK_HOST: 'http://localhost:' + port, SNYK_TOKEN: '123456789', SNYK_DISABLE_ANALYTICS: '1', + SNYK_INTERNAL_ORGID: 'orgid-sbom-monitor-cli', }; server = fakeServer(baseApi, env.SNYK_TOKEN); server.listen(port, () => { diff --git a/test/jest/acceptance/snyk-sbom/all-projects.spec.ts b/test/jest/acceptance/snyk-sbom/all-projects.spec.ts index 5252c94d54..b9682b0410 100644 --- a/test/jest/acceptance/snyk-sbom/all-projects.spec.ts +++ b/test/jest/acceptance/snyk-sbom/all-projects.spec.ts @@ -18,6 +18,7 @@ describe('snyk sbom --all-projects (mocked server only)', () => { SNYK_HOST: 'http://localhost:' + port, SNYK_TOKEN: '123456789', SNYK_DISABLE_ANALYTICS: '1', + SNYK_CFG_INTERNAL_ORGID: 'orgid-test-cli', }; server = fakeServer(baseApi, env.SNYK_TOKEN); await server.listenPromise(port); diff --git a/test/jest/acceptance/snyk-test/all-projects.spec.ts b/test/jest/acceptance/snyk-test/all-projects.spec.ts index 25b855d903..14595fc3a2 100644 --- a/test/jest/acceptance/snyk-test/all-projects.spec.ts +++ b/test/jest/acceptance/snyk-test/all-projects.spec.ts @@ -24,6 +24,7 @@ describe('snyk test --all-projects (mocked server only)', () => { SNYK_HOST: 'http://localhost:' + port, SNYK_TOKEN: '123456789', SNYK_DISABLE_ANALYTICS: '1', + SNYK_CFG_INTERNAL_ORGID: 'orgid-test-cli', }; server = fakeServer(baseApi, env.SNYK_TOKEN); server.listen(port, () => { @@ -324,7 +325,7 @@ describe('snyk test --all-projects (mocked server only)', () => { return req.url.includes('/api/v1/test'); }); - expect(backendRequests).toHaveLength(10); + expect(backendRequests).toHaveLength(6); backendRequests.forEach((req: any) => { expect(req.method).toEqual('POST'); expect(req.headers['x-snyk-cli-version']).not.toBeUndefined(); @@ -427,7 +428,7 @@ describe('snyk test --all-projects (mocked server only)', () => { const app1Path = join('app1', 'package.json'); const app2Path = join('app2', 'package.json'); - expect(backendRequests.length).toBe(3); + expect(backendRequests.length).toBe(1); expect(stdout).not.toMatch(sharedPath); expect(stdout).toMatch(app1Path); expect(stdout).toMatch(app2Path); @@ -453,7 +454,7 @@ describe('snyk test --all-projects (mocked server only)', () => { return req.url.includes('/api/v1/test'); }); - expect(backendRequests.length).toBe(3); + expect(backendRequests.length).toBe(1); expect(stdout).not.toMatch(join('shared', 'package.json')); expect(code).toEqual(0); }); @@ -474,7 +475,7 @@ describe('snyk test --all-projects (mocked server only)', () => { return req.url.includes('/api/v1/test'); }); - expect(backendRequests.length).toBe(4); + expect(backendRequests.length).toBe(1); expect(stdout).toMatch(join('app1', 'package.json')); expect(stdout).toMatch(join('app2', 'package.json')); expect(stdout).toMatch(join('shared', 'package.json')); diff --git a/test/jest/acceptance/snyk-test/basic-test-all-languages.spec.ts b/test/jest/acceptance/snyk-test/basic-test-all-languages.spec.ts index 20687e940a..20764336a3 100644 --- a/test/jest/acceptance/snyk-test/basic-test-all-languages.spec.ts +++ b/test/jest/acceptance/snyk-test/basic-test-all-languages.spec.ts @@ -60,6 +60,7 @@ describe.each(userJourneyWorkflows)( SNYK_HOST: 'http://localhost:' + port, SNYK_TOKEN: '123456789', SNYK_DISABLE_ANALYTICS: '1', + SNYK_CFG_INTERNAL_ORGID: 'orgid-test-cli', }; server = fakeServer(baseApi, env.SNYK_TOKEN); server.listen(port, () => { diff --git a/test/jest/acceptance/snyk-test/fail-on.spec.ts b/test/jest/acceptance/snyk-test/fail-on.spec.ts index 9e08a2155d..6361a93101 100644 --- a/test/jest/acceptance/snyk-test/fail-on.spec.ts +++ b/test/jest/acceptance/snyk-test/fail-on.spec.ts @@ -17,6 +17,7 @@ describe('snyk test --fail-on', () => { SNYK_API: 'http://localhost:' + apiPort + apiPath, SNYK_TOKEN: '123456789', SNYK_DISABLE_ANALYTICS: '1', + SNYK_CFG_INTERNAL_ORGID: 'orgid-test-cli', }; server = fakeServer(apiPath, env.SNYK_TOKEN); diff --git a/test/jest/acceptance/snyk-test/maven-dverbose.spec.ts b/test/jest/acceptance/snyk-test/maven-dverbose.spec.ts index fce1ce99be..4f7cd35974 100644 --- a/test/jest/acceptance/snyk-test/maven-dverbose.spec.ts +++ b/test/jest/acceptance/snyk-test/maven-dverbose.spec.ts @@ -21,6 +21,7 @@ describe('`snyk test` of basic projects for each language/ecosystem', () => { SNYK_HOST: 'http://localhost:' + port, SNYK_TOKEN: '123456789', SNYK_DISABLE_ANALYTICS: '1', + SNYK_CFG_INTERNAL_ORGID: 'orgid-test-cli', }; server = fakeServer(baseApi, env.SNYK_TOKEN); server.listen(port, () => { diff --git a/test/jest/acceptance/snyk-test/missing-node-modules.spec.ts b/test/jest/acceptance/snyk-test/missing-node-modules.spec.ts index 25fec86731..7bdd6c1171 100644 --- a/test/jest/acceptance/snyk-test/missing-node-modules.spec.ts +++ b/test/jest/acceptance/snyk-test/missing-node-modules.spec.ts @@ -26,6 +26,7 @@ describe('snyk test with missing node_modules', () => { SNYK_HOST: 'http://localhost:' + port, SNYK_TOKEN: requireSnykToken(), SNYK_DISABLE_ANALYTICS: '1', + SNYK_CFG_INTERNAL_ORGID: 'orgid-test-cli', }; const apiKey = '123456789'; diff --git a/test/jest/unit/cli-commands/test-and-monitor.spec.ts b/test/jest/unit/cli-commands/test-and-monitor.spec.ts index 5ecd1b77ba..80f530b513 100644 --- a/test/jest/unit/cli-commands/test-and-monitor.spec.ts +++ b/test/jest/unit/cli-commands/test-and-monitor.spec.ts @@ -1,10 +1,15 @@ import monitor from '../../../../src/cli/commands/monitor'; -import { DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG } from '../../../../src/lib/package-managers'; -import * as featureFlags from '../../../../src/lib/feature-flags'; +import { + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + PNPM_FEATURE_FLAG, +} from '../../../../src/lib/package-managers'; +import * as featureFlagGateway from '../../../../src/lib/feature-flag-gateway'; import { SCAN_USR_LIB_JARS_FEATURE_FLAG, INCLUDE_SYSTEM_JARS_OPTION, EXCLUDE_APP_VULNS_OPTION, + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, } from '../../../../src/cli/commands/constants'; import * as ecosystems from '../../../../src/lib/ecosystems'; import * as analytics from '../../../../src/lib/analytics'; @@ -14,6 +19,7 @@ import { apiOrOAuthTokenExists } from '../../../../src/lib/api-token'; import { runTest } from '../../../../src/lib/snyk-test/run-test'; import * as detect from '../../../../src/lib/detect'; import test from '../../../../src/cli/commands/test'; +import { SHOW_MAVEN_BUILD_SCOPE } from '../../../../src/lib/feature-flag-gateway'; jest.mock('../../../../src/lib/api-token'); jest.mock('../../../../src/lib/check-paths'); @@ -22,7 +28,7 @@ jest.mock('../../../../src/lib/formatters'); jest.mock('../../../../src/lib/plugins/get-deps-from-plugin'); jest.mock('../../../../src/lib/spinner'); jest.mock('../../../../src/lib/snyk-test/run-test'); -jest.mock('../../../../src/lib/feature-flags'); +jest.mock('../../../../src/lib/feature-flag-gateway'); jest.mock('../../../../src/lib/protect-update-notification', () => ({ getPackageJsonPathsContainingSnykDependency: jest.fn(() => []), getProtectUpgradeWarningForPaths: jest.fn(() => ''), @@ -53,7 +59,9 @@ describe('monitor & test', () => { analyticsSpy = jest.spyOn(analytics, 'allowAnalytics'); snykMonitorSpy = jest.spyOn(snykMonitor, 'monitor'); (apiOrOAuthTokenExists as jest.Mock).mockReturnValue(true); - (featureFlags.hasFeatureFlag as jest.Mock).mockResolvedValue(false); + (featureFlagGateway.getEnabledFeatureFlags as jest.Mock).mockResolvedValue( + new Set([]), + ); // mock config values Object.defineProperty(config, 'PROJECT_NAME', { @@ -65,7 +73,9 @@ describe('monitor & test', () => { getEcosystemSpy.mockRestore(); analyticsSpy.mockRestore(); snykMonitorSpy.mockRestore(); - (featureFlags.hasFeatureFlag as jest.Mock).mockResolvedValue(false); + (featureFlagGateway.getEnabledFeatureFlags as jest.Mock).mockResolvedValue( + new Set([]), + ); }); describe('monitor', () => { @@ -75,7 +85,9 @@ describe('monitor & test', () => { const options: any = { 'dotnet-runtime-resolution': true, }; - (featureFlags.hasFeatureFlag as jest.Mock).mockResolvedValue(true); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValue(new Set([DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG])); try { await monitor('path/to/project', options); @@ -84,10 +96,17 @@ describe('monitor & test', () => { // We only care about the options being set correctly. } - expect(featureFlags.hasFeatureFlag).toHaveBeenCalledWith( - DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, - options, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + SCAN_USR_LIB_JARS_FEATURE_FLAG, + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + ], + expect.any(String), ); + expect(options.useImprovedDotnetWithoutPublish).toBe(true); }); @@ -97,7 +116,9 @@ describe('monitor & test', () => { const options: any = { 'dotnet-runtime-resolution': true, }; - (featureFlags.hasFeatureFlag as jest.Mock).mockResolvedValue(false); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValue(new Set()); try { await monitor('path/to/project', options); @@ -106,11 +127,17 @@ describe('monitor & test', () => { // We only care about the options being set correctly. } - expect(featureFlags.hasFeatureFlag).toHaveBeenCalledWith( - DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, - options, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + SCAN_USR_LIB_JARS_FEATURE_FLAG, + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + ], + expect.any(String), ); - expect(options.useImprovedDotnetWithoutPublish).toBeUndefined(); + expect(options.useImprovedDotnetWithoutPublish).toBeFalsy(); }); it('should not check the feature flag if dotnet-runtime-resolution is not enabled', async () => { @@ -125,9 +152,15 @@ describe('monitor & test', () => { // We only care about the options being set correctly. } - expect(featureFlags.hasFeatureFlag).not.toHaveBeenCalledWith( - DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, - options, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + SCAN_USR_LIB_JARS_FEATURE_FLAG, + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + ], + expect.any(String), ); expect(options.useImprovedDotnetWithoutPublish).toBeUndefined(); }); @@ -143,12 +176,21 @@ describe('monitor & test', () => { const options: any = { 'dotnet-runtime-resolution': true, }; - (featureFlags.hasFeatureFlag as jest.Mock).mockResolvedValue(true); + + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValue(new Set([DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG])); + await snykTest('path/to/project', options); - expect(featureFlags.hasFeatureFlag).toHaveBeenCalledWith( - DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, - options, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + SHOW_MAVEN_BUILD_SCOPE, + ], + expect.any(String), ); expect(options.useImprovedDotnetWithoutPublish).toBe(true); }); @@ -157,12 +199,19 @@ describe('monitor & test', () => { const options: any = { 'dotnet-runtime-resolution': true, }; - (featureFlags.hasFeatureFlag as jest.Mock).mockResolvedValue(false); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValue(new Set()); await snykTest('path/to/project', options); - expect(featureFlags.hasFeatureFlag).toHaveBeenCalledWith( - DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, - options, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + SHOW_MAVEN_BUILD_SCOPE, + ], + expect.any(String), ); expect(options.useImprovedDotnetWithoutPublish).toBeUndefined(); }); @@ -171,9 +220,14 @@ describe('monitor & test', () => { const options: any = {}; await snykTest('path/to/project', options); - expect(featureFlags.hasFeatureFlag).not.toHaveBeenCalledWith( - DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, - options, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + SHOW_MAVEN_BUILD_SCOPE, + ], + expect.any(String), ); expect(options.useImprovedDotnetWithoutPublish).toBeUndefined(); }); @@ -210,22 +264,17 @@ describe('monitor & test', () => { }; // Mock feature flag responses - need to handle multiple calls - (featureFlags.hasFeatureFlag as jest.Mock).mockImplementation( - (flag: string) => { - if (flag === SCAN_USR_LIB_JARS_FEATURE_FLAG) { - return Promise.resolve(true); - } - return Promise.resolve(false); // Default for other flags - }, - ); - (featureFlags.hasFeatureFlagOrDefault as jest.Mock).mockImplementation( - (flag: string) => { - if (flag === SCAN_USR_LIB_JARS_FEATURE_FLAG) { - return Promise.resolve(true); - } - return Promise.resolve(false); // Default for other flags - }, - ); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockImplementation(async (flags: string[]) => { + const enabled = new Set(); + + if (flags.includes(SCAN_USR_LIB_JARS_FEATURE_FLAG)) { + enabled.add(SCAN_USR_LIB_JARS_FEATURE_FLAG); + } + + return enabled; + }); try { await test('docker-image:latest', options); @@ -234,12 +283,15 @@ describe('monitor & test', () => { // We only care about the feature flag being called correctly. } - expect(featureFlags.hasFeatureFlagOrDefault).toHaveBeenCalledWith( - SCAN_USR_LIB_JARS_FEATURE_FLAG, - expect.objectContaining({ - docker: true, - }), - false, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + SCAN_USR_LIB_JARS_FEATURE_FLAG, + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + ], + expect.any(String), ); // Verify that include-system-jars was set on the internal options object passed to runTest @@ -254,14 +306,9 @@ describe('monitor & test', () => { }; // Mock feature flag responses - need to handle multiple calls - (featureFlags.hasFeatureFlag as jest.Mock).mockImplementation(() => { - return Promise.resolve(false); // All flags disabled - }); - (featureFlags.hasFeatureFlagOrDefault as jest.Mock).mockImplementation( - () => { - return Promise.resolve(false); // All flags disabled - }, - ); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValue(new Set()); try { await test('docker-image:latest', options); @@ -270,12 +317,15 @@ describe('monitor & test', () => { // We only care about the feature flag being called correctly. } - expect(featureFlags.hasFeatureFlagOrDefault).toHaveBeenCalledWith( - SCAN_USR_LIB_JARS_FEATURE_FLAG, - expect.objectContaining({ - docker: true, - }), - false, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + SCAN_USR_LIB_JARS_FEATURE_FLAG, + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + ], + expect.any(String), ); // Verify that include-system-jars was NOT set when the feature flag is disabled @@ -294,10 +344,15 @@ describe('monitor & test', () => { // We only care about the options being set correctly. } - expect(featureFlags.hasFeatureFlagOrDefault).not.toHaveBeenCalledWith( - SCAN_USR_LIB_JARS_FEATURE_FLAG, - options, - false, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + SCAN_USR_LIB_JARS_FEATURE_FLAG, + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + ], + expect.any(String), ); expect(options[INCLUDE_SYSTEM_JARS_OPTION]).toBeUndefined(); }); @@ -311,22 +366,17 @@ describe('monitor & test', () => { }; // Mock feature flag responses - need to handle multiple calls - (featureFlags.hasFeatureFlag as jest.Mock).mockImplementation( - (flag: string) => { - if (flag === SCAN_USR_LIB_JARS_FEATURE_FLAG) { - return Promise.resolve(true); - } - return Promise.resolve(false); // Default for other flags - }, - ); - (featureFlags.hasFeatureFlagOrDefault as jest.Mock).mockImplementation( - (flag: string) => { - if (flag === SCAN_USR_LIB_JARS_FEATURE_FLAG) { - return Promise.resolve(true); - } - return Promise.resolve(false); // Default for other flags - }, - ); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockImplementation(async (flags: string[]) => { + const enabled = new Set(); + + if (flags.includes(SCAN_USR_LIB_JARS_FEATURE_FLAG)) { + enabled.add(SCAN_USR_LIB_JARS_FEATURE_FLAG); + } + + return enabled; + }); try { await monitor('docker-image:latest', options); @@ -335,12 +385,15 @@ describe('monitor & test', () => { // We only care about the feature flag being called correctly. } - expect(featureFlags.hasFeatureFlagOrDefault).toHaveBeenCalledWith( - SCAN_USR_LIB_JARS_FEATURE_FLAG, - expect.objectContaining({ - docker: true, - }), - false, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + SCAN_USR_LIB_JARS_FEATURE_FLAG, + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + ], + expect.any(String), ); // Verify that include-system-jars was set on the options object @@ -354,14 +407,9 @@ describe('monitor & test', () => { }; // Mock feature flag responses - need to handle multiple calls - (featureFlags.hasFeatureFlag as jest.Mock).mockImplementation(() => { - return Promise.resolve(false); // All flags disabled - }); - (featureFlags.hasFeatureFlagOrDefault as jest.Mock).mockImplementation( - () => { - return Promise.resolve(false); // All flags disabled - }, - ); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValue(new Set()); try { await monitor('docker-image:latest', options); @@ -370,12 +418,15 @@ describe('monitor & test', () => { // We only care about the feature flag being called correctly. } - expect(featureFlags.hasFeatureFlagOrDefault).toHaveBeenCalledWith( - SCAN_USR_LIB_JARS_FEATURE_FLAG, - expect.objectContaining({ - docker: true, - }), - false, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + SCAN_USR_LIB_JARS_FEATURE_FLAG, + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + ], + expect.any(String), ); // Verify that include-system-jars was NOT set when the feature flag is disabled @@ -394,10 +445,15 @@ describe('monitor & test', () => { // We only care about the options being set correctly. } - expect(featureFlags.hasFeatureFlagOrDefault).not.toHaveBeenCalledWith( - SCAN_USR_LIB_JARS_FEATURE_FLAG, - options, - false, + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + [ + CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + SCAN_USR_LIB_JARS_FEATURE_FLAG, + PNPM_FEATURE_FLAG, + DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG, + MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, + ], + expect.any(String), ); expect(options[INCLUDE_SYSTEM_JARS_OPTION]).toBeUndefined(); }); diff --git a/test/jest/unit/lib/commands/fix/fix.spec.ts b/test/jest/unit/lib/commands/fix/fix.spec.ts index 3136e8d82d..d609ffae7b 100644 --- a/test/jest/unit/lib/commands/fix/fix.spec.ts +++ b/test/jest/unit/lib/commands/fix/fix.spec.ts @@ -3,12 +3,14 @@ import * as fs from 'fs'; import * as snykFix from '@snyk/fix'; import fix from '../../../../../../src/cli/commands/fix'; import * as snyk from '../../../../../../src/lib'; -import * as featureFlags from '../../../../../../src/lib/feature-flags'; +import * as featureFlagGateway from '../../../../../../src/lib/feature-flag-gateway'; import * as analytics from '../../../../../../src/lib/analytics'; import stripAnsi = require('strip-ansi'); import { getWorkspacePath } from '../../../../util/getWorkspacePath'; import { getFixturePath } from '../../../../util/getFixturePath'; +jest.mock('../../../../../../src/lib/feature-flag-gateway'); + const testTimeout = 100000; const pipAppWorkspace = getWorkspacePath('pip-app'); @@ -37,9 +39,10 @@ describe('snyk fix (functional tests)', () => { beforeAll(async () => { origStdWrite = process.stdout.write; - jest - .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') - .mockResolvedValue({ ok: true }); + jest.clearAllMocks(); + (featureFlagGateway.getEnabledFeatureFlags as jest.Mock).mockResolvedValue( + new Set(['cliSnykFix']), + ); }); beforeEach(() => { diff --git a/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts b/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts index e508782472..ac4e6582d9 100644 --- a/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts +++ b/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts @@ -1,7 +1,6 @@ import { Options } from '../../../../../src/lib/types'; import { v4 as uuidv4 } from 'uuid'; import * as pollingTest from '../../../../../src/lib/polling/polling-test'; -import * as featureFlags from '../../../../../src/lib/feature-flags/index'; import * as common from '../../../../../src/lib/polling/common'; import { scanResults } from './fixtures/'; import { resolveAndTestFacts } from '../../../../../src/lib/ecosystems/resolve-test-facts'; @@ -39,11 +38,6 @@ describe('resolve and test facts', () => { afterEach(() => jest.restoreAllMocks()); it('successfully resolving and testing file-signatures fact for c/c++ projects with unmanaged-deps service', async () => { - const hasFeatureFlag: boolean | undefined = true; - jest - .spyOn(featureFlags, 'hasFeatureFlag') - .mockResolvedValueOnce(hasFeatureFlag); - jest.spyOn(common, 'delayNextStep').mockImplementation(); jest.spyOn(pollingTest, 'createDepGraph').mockResolvedValueOnce({ @@ -75,11 +69,6 @@ describe('resolve and test facts', () => { }); it('successfully resolving and testing file-signatures fact for c/c++ projects with unmanaged-deps service when org slug is provided', async () => { - const hasFeatureFlag: boolean | undefined = true; - jest - .spyOn(featureFlags, 'hasFeatureFlag') - .mockResolvedValueOnce(hasFeatureFlag); - jest.spyOn(common, 'delayNextStep').mockImplementation(); jest.spyOn(pollingTest, 'createDepGraph').mockResolvedValueOnce({ @@ -111,11 +100,6 @@ describe('resolve and test facts', () => { }); it('successfully resolving and testing file-signatures fact after a retry for c/c++ projects with unmanaged-deps service', async () => { - const hasFeatureFlag: boolean | undefined = true; - jest - .spyOn(featureFlags, 'hasFeatureFlag') - .mockResolvedValueOnce(hasFeatureFlag); - jest.spyOn(common, 'delayNextStep').mockImplementation(); jest.spyOn(pollingTest, 'createDepGraph').mockResolvedValueOnce({ @@ -153,11 +137,6 @@ describe('resolve and test facts', () => { }); it('failed resolving and testing file-signatures since createDepGraph throws exception with unmanaged-deps service', async () => { - const hasFeatureFlag: boolean | undefined = true; - jest - .spyOn(featureFlags, 'hasFeatureFlag') - .mockResolvedValueOnce(hasFeatureFlag); - jest.spyOn(common, 'delayNextStep').mockImplementation(); jest.spyOn(pollingTest, 'createDepGraph').mockImplementation(() => { @@ -175,11 +154,6 @@ describe('resolve and test facts', () => { }); it('failed resolving and testing file-signatures since getDepGraph throws exception with unmanaged-deps service', async () => { - const hasFeatureFlag: boolean | undefined = true; - jest - .spyOn(featureFlags, 'hasFeatureFlag') - .mockResolvedValueOnce(hasFeatureFlag); - jest.spyOn(common, 'delayNextStep').mockImplementation(); jest.spyOn(pollingTest, 'createDepGraph').mockResolvedValueOnce({ @@ -203,11 +177,6 @@ describe('resolve and test facts', () => { }); it('failed resolving and testing file-signatures since getIssues throws exception with unmanaged-deps service', async () => { - const hasFeatureFlag: boolean | undefined = true; - jest - .spyOn(featureFlags, 'hasFeatureFlag') - .mockResolvedValueOnce(hasFeatureFlag); - jest.spyOn(common, 'delayNextStep').mockImplementation(); jest.spyOn(pollingTest, 'createDepGraph').mockResolvedValueOnce({ @@ -237,11 +206,6 @@ describe('resolve and test facts', () => { }); it('successfully filters ignored vulnerabilities and includes them in filtered.ignore for unmanaged projects', async () => { - const hasFeatureFlag: boolean | undefined = true; - jest - .spyOn(featureFlags, 'hasFeatureFlag') - .mockResolvedValueOnce(hasFeatureFlag); - jest.spyOn(common, 'delayNextStep').mockImplementation(); jest.spyOn(pollingTest, 'createDepGraph').mockResolvedValueOnce({ diff --git a/test/jest/unit/lib/feature-flag-gateway/feature-flag-gateway.spec.ts b/test/jest/unit/lib/feature-flag-gateway/feature-flag-gateway.spec.ts new file mode 100644 index 0000000000..b949078d02 --- /dev/null +++ b/test/jest/unit/lib/feature-flag-gateway/feature-flag-gateway.spec.ts @@ -0,0 +1,290 @@ +import { + evaluateFeatureFlagsForOrg, + getEnabledFeatureFlags, +} from '../../../../../src/lib/feature-flag-gateway'; +import config from '../../../../../src/lib/config'; +import * as apiToken from '../../../../../src/lib/api-token'; +import { makeRequest } from '../../../../../src/lib/request'; +import { + AuthFailedError, + ValidationError, +} from '../../../../../src/lib/errors'; +import { TestLimitReachedError } from '../../../../../src/cli/commands/test/iac/local-execution/usage-tracking'; + +jest.mock('../../../../../src/lib/request'); + +describe('feature-flag-gateway client', () => { + const orgId = 'f1900900-3db1-4bed-b873-78f589d5a59c'; + const version = '2024-10-15'; + const makeRequestMock = makeRequest as unknown as jest.Mock; + + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + describe('evaluateFeatureFlagsForOrg', () => { + it('should POST to /hidden/orgs/:orgId/feature_flags/evaluation with version querystring and return parsed body', async () => { + const flags = ['show-maven-build-scope', 'snykCode']; + + const mockRespBody = { + data: { + attributes: { + evaluations: [ + { key: 'show-maven-build-scope', value: true, reason: 'found' }, + { key: 'snykCode', value: false, reason: 'found' }, + ], + evaluatedAt: '2025-01-01T00:00:00Z', + }, + }, + }; + + jest.spyOn(apiToken, 'getAuthHeader').mockReturnValue('token test-token'); + makeRequestMock.mockResolvedValue({ + res: { statusCode: 200, statusMessage: 'OK' }, + body: mockRespBody, + }); + + const result = await evaluateFeatureFlagsForOrg(flags, orgId, version); + + expect(result).toEqual(mockRespBody); + + expect(makeRequestMock).toHaveBeenCalledTimes(1); + const [payload] = makeRequestMock.mock.calls[0]; + + expect(payload.url).toBe( + `${config.API_HIDDEN_URL}/orgs/${encodeURIComponent( + orgId, + )}/feature_flags/evaluation?version=${encodeURIComponent(version)}`, + ); + + expect(payload).toEqual( + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.api+json', + Authorization: 'token test-token', + }, + json: true, + noCompression: true, + }), + ); + + expect(payload.body).toEqual( + expect.objectContaining({ + data: { + type: 'feature_flags_evaluation', + attributes: expect.objectContaining({ + flags, + }), + }, + }), + ); + }); + + it('should use default version when not provided', async () => { + const flags = ['show-maven-build-scope']; + + jest.spyOn(apiToken, 'getAuthHeader').mockReturnValue('token test-token'); + makeRequestMock.mockResolvedValue({ + res: { statusCode: 200, statusMessage: 'OK' }, + body: { data: { attributes: { evaluations: [] } } }, + }); + + await evaluateFeatureFlagsForOrg(flags, orgId); + + const [payload] = makeRequestMock.mock.calls[0]; + expect(payload.url as string).toContain('version=2024-10-15'); + }); + + it('should throw if orgId is missing', async () => { + await expect( + evaluateFeatureFlagsForOrg(['show-maven-build-scope'], '' as any), + ).rejects.toThrow('orgId is required'); + + expect(makeRequestMock).not.toHaveBeenCalled(); + }); + + it('should surface network errors from makeRequest', async () => { + makeRequestMock.mockRejectedValue(new Error('Network error')); + + await expect( + evaluateFeatureFlagsForOrg(['show-maven-build-scope'], orgId), + ).rejects.toThrow('Network error'); + }); + + it('should map 401 to AuthFailedError', async () => { + makeRequestMock.mockResolvedValue({ + res: { statusCode: 401, statusMessage: 'Unauthorized' }, + body: {}, + }); + + await expect( + evaluateFeatureFlagsForOrg(['show-maven-build-scope'], orgId), + ).rejects.toThrow(AuthFailedError().message); + }); + + it('should map 429 to TestLimitReachedError', async () => { + makeRequestMock.mockResolvedValue({ + res: { statusCode: 429, statusMessage: 'Too Many Requests' }, + body: {}, + }); + + await expect( + evaluateFeatureFlagsForOrg(['show-maven-build-scope'], orgId), + ).rejects.toBeInstanceOf(TestLimitReachedError); + }); + + it('should map other non-200 status codes to ValidationError with body.error when present', async () => { + makeRequestMock.mockResolvedValue({ + res: { + statusCode: 400, + statusMessage: 'Bad Request', + body: { error: 'Something went wrong' }, + }, + body: { error: 'Something went wrong' }, + }); + + await expect( + evaluateFeatureFlagsForOrg(['show-maven-build-scope'], orgId), + ).rejects.toBeInstanceOf(ValidationError); + + await expect( + evaluateFeatureFlagsForOrg(['show-maven-build-scope'], orgId), + ).rejects.toThrow('Something went wrong'); + }); + }); + + describe('getEnabledFeatureFlags', () => { + it('should return empty set if orgId is empty', async () => { + const result = await getEnabledFeatureFlags(['test-ff'], ''); + expect(result).toEqual(new Set()); + + expect(makeRequestMock).not.toHaveBeenCalled(); + }); + + it('should return empty set if evaluation call throws', async () => { + makeRequestMock.mockRejectedValue(new Error('Boom')); + + const result = await getEnabledFeatureFlags(['test-ff'], orgId); + expect(result).toEqual(new Set()); + }); + + it('should return empty set if evaluations is missing', async () => { + makeRequestMock.mockResolvedValue({ + res: { statusCode: 200, statusMessage: 'OK' }, + body: { + data: { + attributes: {}, + }, + }, + }); + + const result = await getEnabledFeatureFlags(['test-ff'], orgId); + expect(result).toEqual(new Set()); + }); + + it('should return empty set if evaluations is empty', async () => { + makeRequestMock.mockResolvedValue({ + res: { statusCode: 200, statusMessage: 'OK' }, + body: { + data: { + attributes: { + evaluations: [], + }, + }, + }, + }); + + const result = await getEnabledFeatureFlags(['test-ff'], orgId); + expect(result).toEqual(new Set()); + }); + + it('should call gateway with [flag] in context', async () => { + jest.spyOn(apiToken, 'getAuthHeader').mockReturnValue('token test-token'); + + makeRequestMock.mockResolvedValue({ + res: { statusCode: 200, statusMessage: 'OK' }, + body: { + data: { + attributes: { + evaluations: [{ key: 'test-ff', value: true, reason: 'found' }], + }, + }, + }, + }); + + const result = await getEnabledFeatureFlags(['test-ff'], orgId); + expect(result.has('test-ff')).toBe(true); + + expect(makeRequestMock).toHaveBeenCalledTimes(1); + const [payload] = makeRequestMock.mock.calls[0]; + + expect(payload.body.data.attributes.flags).toEqual(['test-ff']); + + expect(payload.url).toEqual( + `${config.API_HIDDEN_URL}/orgs/${encodeURIComponent( + orgId, + )}/feature_flags/evaluation?version=2024-10-15`, + ); + }); + + it.each` + value | expectedPresent + ${true} | ${true} + ${false} | ${false} + ${undefined} | ${false} + `( + 'should include flag in set = $expectedPresent when evaluation value=$value', + async ({ value, expectedPresent }) => { + const evaluations = + value === undefined + ? [{ key: 'other-flag', value: true, reason: 'found' }] + : [{ key: 'test-ff', value, reason: 'found' }]; + + makeRequestMock.mockResolvedValue({ + res: { statusCode: 200, statusMessage: 'OK' }, + body: { + data: { + attributes: { + evaluations, + }, + }, + }, + }); + + const result = await getEnabledFeatureFlags(['test-ff'], orgId); + expect(result.has('test-ff')).toBe(expectedPresent); + + if (value === undefined) { + expect(result.has('other-flag')).toBe(true); + } + }, + ); + + it('should support a list of flags and only include those evaluated as true', async () => { + const flags = ['flag-a', 'flag-b', 'flag-c', 'flag-d']; + + makeRequestMock.mockResolvedValue({ + res: { statusCode: 200, statusMessage: 'OK' }, + body: { + data: { + attributes: { + evaluations: [ + { key: 'flag-a', value: true, reason: 'found' }, + { key: 'flag-b', value: false, reason: 'found' }, + { key: 'flag-c', value: true, reason: 'found' }, + ], + }, + }, + }, + }); + + const result = await getEnabledFeatureFlags(flags, orgId); + + expect(result).toEqual(new Set(['flag-a', 'flag-c'])); + expect(result.has('flag-b')).toBe(false); + expect(result.has('flag-d')).toBe(false); + }); + }); +}); diff --git a/test/jest/unit/lib/feature-flags/feature-flags.spec.ts b/test/jest/unit/lib/feature-flags/feature-flags.spec.ts deleted file mode 100644 index 02dbd37eca..0000000000 --- a/test/jest/unit/lib/feature-flags/feature-flags.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { hasFeatureFlag } from '../../../../../src/lib/feature-flags'; -import * as request from '../../../../../src/lib/request'; - -describe('hasFeatureFlag fn', () => { - it.each` - hasFlag | expected - ${true} | ${true} - ${false} | ${false} - `( - 'should validate that given an org with feature flag $hasFlag as input, hasFeatureFlag returns $expected', - async ({ hasFlag, expected }) => { - jest - .spyOn(request, 'makeRequest') - .mockResolvedValue({ body: { code: 200, ok: hasFlag } } as any); - - const result = await hasFeatureFlag('test-ff', { path: 'test-path' }); - expect(result).toEqual(expected); - }, - ); - - it('should throw error if there are authentication/authorization failures', async () => { - jest.spyOn(request, 'makeRequest').mockResolvedValue({ - body: { code: 401, error: 'Unauthorized', ok: false }, - } as any); - - await expect( - hasFeatureFlag('test-ff', { path: 'test-path' }), - ).rejects.toThrowError('Unauthorized'); - - jest.spyOn(request, 'makeRequest').mockResolvedValue({ - body: { code: 403, error: 'Forbidden', ok: false }, - } as any); - await expect( - hasFeatureFlag('test-ff', { path: 'test-path' }), - ).rejects.toThrowError('Forbidden'); - }); -}); diff --git a/test/jest/unit/pnpm/snyk-test-pnpm-project.spec.ts b/test/jest/unit/pnpm/snyk-test-pnpm-project.spec.ts index d66bef9642..96e91dd2e5 100644 --- a/test/jest/unit/pnpm/snyk-test-pnpm-project.spec.ts +++ b/test/jest/unit/pnpm/snyk-test-pnpm-project.spec.ts @@ -2,24 +2,28 @@ import { NeedleResponse } from 'needle'; import test from '../../../../src/cli/commands/test'; import { CommandResult } from '../../../../src/cli/commands/types'; import { makeRequest } from '../../../../src/lib/request/request'; -import * as featureFlagsModule from '../../../../src/lib/feature-flags'; +import * as featureFlagGateway from '../../../../src/lib/feature-flag-gateway'; + import { getFixturePath } from '../../util/getFixturePath'; +import { PNPM_FEATURE_FLAG } from '../../../../src/lib/package-managers'; jest.mock('../../../../src/lib/request/request'); +jest.mock('../../../../src/lib/feature-flag-gateway'); const mockedMakeRequest = jest.mocked(makeRequest); describe('snyk test for pnpm project', () => { afterEach(() => { jest.clearAllMocks(); + (featureFlagGateway.getEnabledFeatureFlags as jest.Mock).mockResolvedValue( + new Set(), + ); }); beforeAll(() => { // this spy is for the `cliFailFast` feature flag jest - .spyOn(featureFlagsModule, 'isFeatureFlagSupportedForOrg') - .mockResolvedValue({ - ok: true, - }); + .spyOn(featureFlagGateway, 'getEnabledFeatureFlags') + .mockResolvedValue(new Set()); }); afterAll(() => { @@ -31,16 +35,9 @@ describe('snyk test for pnpm project', () => { it('should scan pnpm vulnerabilities when enablePnpmCli feature flag is enabled', async () => { const fixturePath = getFixturePath('pnpm-app'); - // this is for 'enablePnpmCli' feature flag - mockedMakeRequest.mockImplementationOnce(() => { - return Promise.resolve({ - res: { statusCode: 200 } as NeedleResponse, - body: { - code: 200, - ok: true, - }, - }); - }); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValue(new Set([PNPM_FEATURE_FLAG])); mockedMakeRequest.mockImplementationOnce(() => { return Promise.resolve({ @@ -59,7 +56,11 @@ describe('snyk test for pnpm project', () => { _doubleDashArgs: [], }); - expect(mockedMakeRequest).toHaveBeenCalledTimes(2); + expect(featureFlagGateway.getEnabledFeatureFlags).toHaveBeenCalledWith( + expect.any(Array), + expect.any(String), + ); + expect(mockedMakeRequest).toHaveBeenCalledTimes(1); expect(mockedMakeRequest).toHaveBeenCalledWith( expect.objectContaining({ body: expect.objectContaining({ @@ -101,15 +102,9 @@ describe('snyk test for pnpm project', () => { const fixturePath = getFixturePath('pnpm-app'); // this is for 'enablePnpmCli' feature flag - mockedMakeRequest.mockImplementationOnce(() => { - return Promise.resolve({ - res: { statusCode: 200 } as NeedleResponse, - body: { - code: 200, - ok: false, - }, - }); - }); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValueOnce(new Set([])); mockedMakeRequest.mockImplementationOnce(() => { return Promise.resolve({ @@ -128,7 +123,7 @@ describe('snyk test for pnpm project', () => { _doubleDashArgs: [], }); - expect(mockedMakeRequest).toHaveBeenCalledTimes(2); + expect(mockedMakeRequest).toHaveBeenCalledTimes(1); const expectedResultObject = { vulnerabilities: [], @@ -167,15 +162,9 @@ describe('snyk test for pnpm project', () => { const fixturePath = getFixturePath('workspace-multi-type'); // this is for 'enablePnpmCli' feature flag - mockedMakeRequest.mockImplementationOnce(() => { - return Promise.resolve({ - res: { statusCode: 200 } as NeedleResponse, - body: { - code: 200, - ok: true, - }, - }); - }); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValue(new Set([PNPM_FEATURE_FLAG])); mockedMakeRequest.mockImplementation(() => { return Promise.resolve({ @@ -195,7 +184,7 @@ describe('snyk test for pnpm project', () => { _doubleDashArgs: [], }); - expect(mockedMakeRequest).toHaveBeenCalledTimes(11); + expect(mockedMakeRequest).toHaveBeenCalledTimes(10); const parsedResult = JSON.parse(result.getDisplayResults()); const pnpmResult = parsedResult.filter( @@ -210,15 +199,9 @@ describe('snyk test for pnpm project', () => { const fixturePath = getFixturePath('workspace-multi-type'); // this is for 'enablePnpmCli' feature flag - mockedMakeRequest.mockImplementationOnce(() => { - return Promise.resolve({ - res: { statusCode: 200 } as NeedleResponse, - body: { - code: 200, - ok: false, - }, - }); - }); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValueOnce(new Set()); mockedMakeRequest.mockImplementation(() => { return Promise.resolve({ @@ -238,7 +221,7 @@ describe('snyk test for pnpm project', () => { _doubleDashArgs: [], }); - expect(mockedMakeRequest).toHaveBeenCalledTimes(7); + expect(mockedMakeRequest).toHaveBeenCalledTimes(6); const parsedResult = JSON.parse(result.getDisplayResults()); const pnpmResult = parsedResult.filter( diff --git a/test/jest/unit/python/snyk-test-pyproject.spec.ts b/test/jest/unit/python/snyk-test-pyproject.spec.ts index 183b7fe1df..bf866ba641 100644 --- a/test/jest/unit/python/snyk-test-pyproject.spec.ts +++ b/test/jest/unit/python/snyk-test-pyproject.spec.ts @@ -3,7 +3,7 @@ import test from '../../../../src/cli/commands/test'; import { loadPlugin } from '../../../../src/lib/plugins/index'; import { CommandResult } from '../../../../src/cli/commands/types'; import { makeRequest } from '../../../../src/lib/request/request'; -import * as featureFlagsModule from '../../../../src/lib/feature-flags'; +import * as featureFlagGatewayModule from '../../../../src/lib/feature-flag-gateway'; import { getWorkspacePath } from '../../util/getWorkspacePath'; import { getFixturePath } from '../../util/getFixturePath'; @@ -21,10 +21,8 @@ describe('snyk test for python project', () => { beforeAll(() => { // this spy is for the `cliFailFast` feature flag jest - .spyOn(featureFlagsModule, 'isFeatureFlagSupportedForOrg') - .mockResolvedValue({ - ok: false, - }); + .spyOn(featureFlagGatewayModule, 'getEnabledFeatureFlags') + .mockResolvedValue(new Set()); }); afterAll(() => { @@ -50,15 +48,6 @@ describe('snyk test for python project', () => { }; // this is for 'enablePnpmCli' feature flag - mockedMakeRequest.mockImplementationOnce(() => { - return Promise.resolve({ - res: { statusCode: 200 } as NeedleResponse, - body: { - code: 200, - ok: false, - }, - }); - }); mockedLoadPlugin.mockImplementationOnce(() => { return plugin; }); @@ -82,7 +71,7 @@ describe('snyk test for python project', () => { expect(mockedLoadPlugin).toHaveBeenCalledTimes(1); expect(mockedLoadPlugin).toHaveBeenCalledWith('poetry'); - expect(mockedMakeRequest).toHaveBeenCalledTimes(2); + expect(mockedMakeRequest).toHaveBeenCalledTimes(1); expect(mockedMakeRequest).toHaveBeenCalledWith( expect.objectContaining({ body: expect.objectContaining({ @@ -136,16 +125,6 @@ describe('snyk test for python project', () => { }, }; - // this is for 'enablePnpmCli' feature flag - mockedMakeRequest.mockImplementationOnce(() => { - return Promise.resolve({ - res: { statusCode: 200 } as NeedleResponse, - body: { - code: 200, - ok: false, - }, - }); - }); mockedLoadPlugin.mockImplementationOnce(() => { return plugin; }); @@ -169,7 +148,7 @@ describe('snyk test for python project', () => { expect(mockedLoadPlugin).toHaveBeenCalledTimes(1); expect(mockedLoadPlugin).toHaveBeenCalledWith('poetry'); - expect(mockedMakeRequest).toHaveBeenCalledTimes(2); + expect(mockedMakeRequest).toHaveBeenCalledTimes(1); expect(mockedMakeRequest).toHaveBeenCalledWith( expect.objectContaining({ body: expect.objectContaining({ @@ -228,15 +207,6 @@ describe('snyk test for python project', () => { }; // this is for 'enablePnpmCli' feature flag - mockedMakeRequest.mockImplementationOnce(() => { - return Promise.resolve({ - res: { statusCode: 200 } as NeedleResponse, - body: { - code: 200, - ok: false, - }, - }); - }); mockedLoadPlugin.mockImplementationOnce(() => { return plugin; }); @@ -261,7 +231,7 @@ describe('snyk test for python project', () => { expect(mockedLoadPlugin).toHaveBeenCalledTimes(1); expect(mockedLoadPlugin).toHaveBeenCalledWith('pip'); - expect(mockedMakeRequest).toHaveBeenCalledTimes(2); + expect(mockedMakeRequest).toHaveBeenCalledTimes(1); expect(mockedMakeRequest).toHaveBeenCalledWith( expect.objectContaining({ body: expect.objectContaining({ @@ -329,16 +299,6 @@ describe('snyk test for python project', () => { }, }; - // this is for 'enablePnpmCli' feature flag - mockedMakeRequest.mockImplementationOnce(() => { - return Promise.resolve({ - res: { statusCode: 200 } as NeedleResponse, - body: { - code: 200, - ok: false, - }, - }); - }); mockedLoadPlugin .mockImplementationOnce(() => pipfilePythonPluginResponse) .mockImplementationOnce(() => pyprojectPythonPluginResponse); @@ -364,7 +324,7 @@ describe('snyk test for python project', () => { expect(mockedLoadPlugin).toHaveBeenCalledWith('pip'); expect(mockedLoadPlugin).toHaveBeenCalledWith('poetry'); - expect(mockedMakeRequest).toHaveBeenCalledTimes(3); + expect(mockedMakeRequest).toHaveBeenCalledTimes(2); expect(mockedMakeRequest).toHaveBeenCalledWith( expect.objectContaining({ body: expect.objectContaining({ @@ -461,17 +421,7 @@ describe('snyk test for python project', () => { }, }; - // Stub feature flag request (enablePnpmCli) - mockedMakeRequest.mockImplementationOnce(() => { - return Promise.resolve({ - res: { statusCode: 200 } as NeedleResponse, - body: { - code: 200, - ok: false, - }, - }); - }); - + // Stub mockedLoadPlugin.mockImplementationOnce(() => { return plugin; }); @@ -498,7 +448,7 @@ describe('snyk test for python project', () => { expect(mockedLoadPlugin).toHaveBeenCalledTimes(1); expect(mockedLoadPlugin).toHaveBeenCalledWith('poetry'); - expect(mockedMakeRequest).toHaveBeenCalledTimes(2); + expect(mockedMakeRequest).toHaveBeenCalledTimes(1); expect(mockedMakeRequest).toHaveBeenCalledWith( expect.objectContaining({ body: expect.objectContaining({ diff --git a/test/jest/unit/validate-fix-command-is-supported.spec.ts b/test/jest/unit/validate-fix-command-is-supported.spec.ts index be39d7bfc6..902b88fb73 100644 --- a/test/jest/unit/validate-fix-command-is-supported.spec.ts +++ b/test/jest/unit/validate-fix-command-is-supported.spec.ts @@ -1,45 +1,48 @@ import { validateFixCommandIsSupported } from '../../../src/cli/commands/fix/validate-fix-command-is-supported'; import { AuthFailedError } from '../../../src/lib/errors'; import { FeatureNotSupportedByEcosystemError } from '../../../src/lib/errors/not-supported-by-ecosystem'; -import * as featureFlags from '../../../src/lib/feature-flags'; +import * as featureFlagGateway from '../../../src/lib/feature-flag-gateway'; import { ShowVulnPaths } from '../../../src/lib/types'; + +jest.mock('../../../src/lib/feature-flag-gateway'); + describe('setDefaultTestOptions', () => { afterEach(() => { jest.clearAllMocks(); }); it('fix is supported for OS projects + enabled FF', () => { - jest - .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') - .mockResolvedValue({ ok: true }); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValueOnce(new Set(['cliSnykFix'])); const options = { path: '/', showVulnPaths: 'all' as ShowVulnPaths }; const supported = validateFixCommandIsSupported(options); expect(supported).toBeTruthy(); }); it('fix is NOT supported for OS projects + disabled FF', async () => { - jest - .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') - .mockResolvedValue({ ok: false }); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValueOnce(new Set()); const options = { path: '/', showVulnPaths: 'all' as ShowVulnPaths }; await expect(validateFixCommandIsSupported(options)).rejects.toThrowError( - '`snyk fix` is not supported', + 'snykFixSupported is false', ); }); it('fix is NOT supported and bubbles up auth error invalid auth token', async () => { - jest - .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') - .mockResolvedValue({ ok: false, code: 401, error: 'Invalid auth token' }); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValueOnce(new Set()); const options = { path: '/', showVulnPaths: 'all' as ShowVulnPaths }; await expect(validateFixCommandIsSupported(options)).rejects.toThrowError( - AuthFailedError('Invalid auth token', 401), + AuthFailedError('snykFixSupported is false', 403), ); }); it('fix is NOT supported for --unmanaged + enabled FF', async () => { - jest - .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') - .mockResolvedValue({ ok: true }); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValueOnce(new Set(['cliSnykFix'])); const options = { path: '/', showVulnPaths: 'all' as ShowVulnPaths, @@ -51,9 +54,9 @@ describe('setDefaultTestOptions', () => { }); it('fix is NOT supported for --docker + enabled FF', async () => { - jest - .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') - .mockResolvedValue({ ok: true }); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValueOnce(new Set(['cliSnykFix'])); const options = { path: '/', showVulnPaths: 'all' as ShowVulnPaths, @@ -65,9 +68,9 @@ describe('setDefaultTestOptions', () => { }); it('fix is NOT supported for --code + enabled FF', async () => { - jest - .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') - .mockResolvedValue({ ok: true }); + ( + featureFlagGateway.getEnabledFeatureFlags as jest.Mock + ).mockResolvedValueOnce(new Set(['cliSnykFix'])); const options = { path: '/', showVulnPaths: 'all' as ShowVulnPaths, diff --git a/test/tap/auth.test.ts b/test/tap/auth.test.ts index f996023894..d5fecbd65a 100644 --- a/test/tap/auth.test.ts +++ b/test/tap/auth.test.ts @@ -13,6 +13,7 @@ const BASE_API = '/api/v1'; process.env.SNYK_API = 'http://localhost:' + port + BASE_API; process.env.SNYK_HOST = 'http://localhost:' + port; process.env.LOG_LEVEL = '0'; +process.env.SNYK_INTERNAL_ORGID = 'orgid-test-auth-cli'; const server = fakeServer(BASE_API, apiKey); diff --git a/test/tap/cli-fail-on-docker.test.ts b/test/tap/cli-fail-on-docker.test.ts index 825eca7ae5..788e2f6cfe 100644 --- a/test/tap/cli-fail-on-docker.test.ts +++ b/test/tap/cli-fail-on-docker.test.ts @@ -13,6 +13,7 @@ const BASE_API = '/api/v1'; process.env.SNYK_API = 'http://localhost:' + port + BASE_API; process.env.SNYK_HOST = 'http://localhost:' + port; process.env.LOG_LEVEL = '0'; +process.env.SNYK_INTERNAL_ORGID = 'orgid-test-fail-on-docker-cli'; const apiKey = '123456789'; let oldkey; let oldendpoint; diff --git a/test/tap/cli-fail-on-pinnable.test.ts b/test/tap/cli-fail-on-pinnable.test.ts index cc4665de17..8ebb901689 100644 --- a/test/tap/cli-fail-on-pinnable.test.ts +++ b/test/tap/cli-fail-on-pinnable.test.ts @@ -16,6 +16,7 @@ const BASE_API = '/api/v1'; process.env.SNYK_API = 'http://localhost:' + port + BASE_API; process.env.SNYK_HOST = 'http://localhost:' + port; process.env.LOG_LEVEL = '0'; +process.env.SNYK_INTERNAL_ORGID = 'orgid-test-fail-on-docker-cli'; const apiKey = '123456789'; let oldkey; let oldendpoint; diff --git a/test/tap/cli-monitor.acceptance.test.ts b/test/tap/cli-monitor.acceptance.test.ts index 6b6dde2469..fa4536e81a 100644 --- a/test/tap/cli-monitor.acceptance.test.ts +++ b/test/tap/cli-monitor.acceptance.test.ts @@ -29,6 +29,7 @@ const BASE_API = '/api/v1'; process.env.SNYK_API = 'http://localhost:' + port + BASE_API; process.env.SNYK_HOST = 'http://localhost:' + port; process.env.LOG_LEVEL = '0'; +process.env.SNYK_INTERNAL_ORGID = 'orgid-test-monitor-cli'; const apiKey = '123456789'; let oldkey; let oldendpoint; @@ -875,6 +876,7 @@ if (!isWindowsOperatingSystem()) { file: 'requirements.txt', packageManager: 'pip', path: 'pip-app', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -939,6 +941,7 @@ if (!isWindowsOperatingSystem()) { file: 'requirements.txt', packageManager: 'pip', path: 'pip-app', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -992,6 +995,7 @@ if (!isWindowsOperatingSystem()) { packageManager: 'gradle', file: 'build.gradle', path: 'gradle-app', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -1062,6 +1066,7 @@ if (!isWindowsOperatingSystem()) { file: 'build.gradle', packageManager: 'gradle', path: 'gradle-app', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -1122,6 +1127,7 @@ if (!isWindowsOperatingSystem()) { file: 'build.gradle', packageManager: 'gradle', path: 'gradle-app', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -1177,6 +1183,7 @@ if (!isWindowsOperatingSystem()) { file: 'build.gradle', packageManager: 'gradle', path: 'gradle-app', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -1193,6 +1200,7 @@ if (!isWindowsOperatingSystem()) { file: 'requirements.txt', packageManager: 'pip', path: 'pip-app', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -1271,6 +1279,7 @@ if (!isWindowsOperatingSystem()) { file: 'go.mod', packageManager: 'gomodules', path: 'golang-gomodules', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -1320,6 +1329,7 @@ if (!isWindowsOperatingSystem()) { file: 'Gopkg.lock', packageManager: 'golangdep', path: 'golang-app', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -1367,6 +1377,7 @@ if (!isWindowsOperatingSystem()) { file: 'Podfile', packageManager: 'cocoapods', path: './', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -1416,6 +1427,7 @@ if (!isWindowsOperatingSystem()) { file: 'Podfile', packageManager: 'cocoapods', path: './', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -1472,6 +1484,7 @@ if (!isWindowsOperatingSystem()) { file: 'Podfile.lock', packageManager: 'cocoapods', path: './', + showMavenBuildScope: false, }, snykHttpClient, ], @@ -1527,6 +1540,7 @@ if (!isWindowsOperatingSystem()) { file: 'Podfile.lock', packageManager: 'cocoapods', path: './', + showMavenBuildScope: false, }, snykHttpClient, ], diff --git a/test/tap/cli-test.acceptance.test.ts b/test/tap/cli-test.acceptance.test.ts index 0340727765..fa3e1ab992 100644 --- a/test/tap/cli-test.acceptance.test.ts +++ b/test/tap/cli-test.acceptance.test.ts @@ -56,6 +56,7 @@ const BASE_API = '/api/v1'; process.env.SNYK_API = 'http://localhost:' + port + BASE_API; process.env.SNYK_HOST = 'http://localhost:' + port; process.env.LOG_LEVEL = '0'; +process.env.SNYK_INTERNAL_ORGID = 'orgid-test-monitor-cli'; const apiKey = '123456789'; let oldkey; let oldendpoint; diff --git a/test/tap/cli-test/cli-test.composer.spec.ts b/test/tap/cli-test/cli-test.composer.spec.ts index f388b669f0..6237cebc91 100644 --- a/test/tap/cli-test/cli-test.composer.spec.ts +++ b/test/tap/cli-test/cli-test.composer.spec.ts @@ -48,6 +48,7 @@ export const ComposerTests: AcceptanceTests = { path: 'composer-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -96,6 +97,7 @@ export const ComposerTests: AcceptanceTests = { path: 'composer-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -148,6 +150,7 @@ export const ComposerTests: AcceptanceTests = { path: 'composer-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -224,6 +227,7 @@ export const ComposerTests: AcceptanceTests = { path: 'composer-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -243,6 +247,7 @@ export const ComposerTests: AcceptanceTests = { path: 'golang-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -262,6 +267,7 @@ export const ComposerTests: AcceptanceTests = { path: 'nuget-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], diff --git a/test/tap/cli-test/cli-test.elixir.spec.ts b/test/tap/cli-test/cli-test.elixir.spec.ts index 72c3537271..d1efc94387 100644 --- a/test/tap/cli-test/cli-test.elixir.spec.ts +++ b/test/tap/cli-test/cli-test.elixir.spec.ts @@ -53,6 +53,7 @@ export const ElixirTests: AcceptanceTests = { path: 'elixir-hex', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -107,6 +108,7 @@ export const ElixirTests: AcceptanceTests = { path: 'elixir-hex', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], diff --git a/test/tap/cli-test/cli-test.go.spec.ts b/test/tap/cli-test/cli-test.go.spec.ts index b804496e7a..8f59e86bd3 100644 --- a/test/tap/cli-test/cli-test.go.spec.ts +++ b/test/tap/cli-test/cli-test.go.spec.ts @@ -52,6 +52,7 @@ export const GoTests: AcceptanceTests = { path: 'golang-gomodules', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -105,6 +106,7 @@ export const GoTests: AcceptanceTests = { path: 'golang-gomodules', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -160,6 +162,7 @@ export const GoTests: AcceptanceTests = { path: 'golang-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -213,6 +216,7 @@ export const GoTests: AcceptanceTests = { path: 'golang-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], diff --git a/test/tap/cli-test/cli-test.maven.spec.ts b/test/tap/cli-test/cli-test.maven.spec.ts index 8e346ebd0d..67e6d00130 100644 --- a/test/tap/cli-test/cli-test.maven.spec.ts +++ b/test/tap/cli-test/cli-test.maven.spec.ts @@ -105,6 +105,7 @@ export const MavenTests: AcceptanceTests = { path: 'maven-app-with-jars', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -157,6 +158,7 @@ export const MavenTests: AcceptanceTests = { path: 'maven-app-with-jars', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -207,6 +209,7 @@ export const MavenTests: AcceptanceTests = { showVulnPaths: 'some', maxVulnPaths: undefined, scanAllUnmanaged: true, + showMavenBuildScope: false, }, snykHttpClient, ], diff --git a/test/tap/cli-test/cli-test.nuget.spec.ts b/test/tap/cli-test/cli-test.nuget.spec.ts index 98a6653a78..25bae7c9c7 100644 --- a/test/tap/cli-test/cli-test.nuget.spec.ts +++ b/test/tap/cli-test/cli-test.nuget.spec.ts @@ -69,6 +69,7 @@ export const NugetTests: AcceptanceTests = { path: 'nuget-app-2', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -121,6 +122,7 @@ export const NugetTests: AcceptanceTests = { path: 'nuget-app-2.1', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -174,6 +176,7 @@ export const NugetTests: AcceptanceTests = { path: 'nuget-app-4', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -229,6 +232,7 @@ export const NugetTests: AcceptanceTests = { path: 'nuget-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -284,6 +288,7 @@ export const NugetTests: AcceptanceTests = { path: 'nuget-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -339,6 +344,7 @@ export const NugetTests: AcceptanceTests = { path: 'nuget-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -392,6 +398,7 @@ export const NugetTests: AcceptanceTests = { path: 'paket-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -445,6 +452,7 @@ export const NugetTests: AcceptanceTests = { path: 'paket-obj-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -500,6 +508,7 @@ export const NugetTests: AcceptanceTests = { path: 'paket-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], diff --git a/test/tap/cli-test/cli-test.python.spec.ts b/test/tap/cli-test/cli-test.python.spec.ts index a12ec2f3be..ecf245e8dd 100644 --- a/test/tap/cli-test/cli-test.python.spec.ts +++ b/test/tap/cli-test/cli-test.python.spec.ts @@ -49,6 +49,7 @@ export const PythonTests: AcceptanceTests = { path: 'pip-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -104,6 +105,7 @@ export const PythonTests: AcceptanceTests = { path: 'pipenv-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -166,6 +168,7 @@ export const PythonTests: AcceptanceTests = { path: 'pip-app-transitive-vuln', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -229,6 +232,7 @@ export const PythonTests: AcceptanceTests = { path: 'pip-app-transitive-vuln', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], @@ -284,6 +288,7 @@ export const PythonTests: AcceptanceTests = { path: 'setup_py-app', showVulnPaths: 'some', maxVulnPaths: undefined, + showMavenBuildScope: false, }, snykHttpClient, ], diff --git a/test/tap/cli.test.ts b/test/tap/cli.test.ts index a99dd79d28..ab4ecf9ede 100644 --- a/test/tap/cli.test.ts +++ b/test/tap/cli.test.ts @@ -35,6 +35,7 @@ const BASE_API = '/api/v1'; process.env.SNYK_API = 'http://localhost:' + port + BASE_API; process.env.SNYK_HOST = 'http://localhost:' + port; process.env.LOG_LEVEL = '0'; +process.env.SNYK_INTERNAL_ORGID = '12345678-1234-1234-1234-123456789012'; const server = fakeServer(BASE_API, apiKey); diff --git a/test/tap/docker-token.test.ts b/test/tap/docker-token.test.ts index 524fa26f4b..f18b63e41f 100644 --- a/test/tap/docker-token.test.ts +++ b/test/tap/docker-token.test.ts @@ -10,6 +10,7 @@ const BASE_API = '/api/v1'; process.env.SNYK_API = 'http://localhost:' + port + BASE_API; process.env.SNYK_HOST = 'http://localhost:' + port; process.env.LOG_LEVEL = '0'; +process.env.SNYK_INTERNAL_ORGID = '12345678-1234-1234-1234-123456789012'; const apiKey = '123456789'; let oldkey; let oldendpoint; diff --git a/test/tap/monitor-target.test.ts b/test/tap/monitor-target.test.ts index 7be63f70d9..0a9666aa15 100644 --- a/test/tap/monitor-target.test.ts +++ b/test/tap/monitor-target.test.ts @@ -15,6 +15,7 @@ const BASE_API = '/api/v1'; process.env.SNYK_API = 'http://localhost:' + port + BASE_API; process.env.SNYK_HOST = 'http://localhost:' + port; process.env.LOG_LEVEL = '0'; +process.env.SNYK_INTERNAL_ORGID = '12345678-1234-1234-1234-123456789012'; let oldkey; let oldendpoint; const server = fakeServer(BASE_API, apiKey); diff --git a/test/tap/remote-package.test.ts b/test/tap/remote-package.test.ts index 54dbcd2659..93340812d1 100644 --- a/test/tap/remote-package.test.ts +++ b/test/tap/remote-package.test.ts @@ -13,6 +13,7 @@ const BASE_API = '/api/v1'; process.env.SNYK_API = 'http://localhost:' + port + BASE_API; process.env.SNYK_HOST = 'http://localhost:' + port; process.env.LOG_LEVEL = '0'; +process.env.SNYK_INTERNAL_ORGID = '12345678-1234-1234-1234-123456789012'; const server = fakeServer(BASE_API, apiKey); diff --git a/test/tap/run-test.test.ts b/test/tap/run-test.test.ts index 5ab808ca80..f71656feff 100644 --- a/test/tap/run-test.test.ts +++ b/test/tap/run-test.test.ts @@ -8,6 +8,7 @@ const BASE_API = '/api/v1'; process.env.SNYK_API = 'http://localhost:' + port + BASE_API; process.env.SNYK_HOST = 'http://localhost:' + port; process.env.LOG_LEVEL = '0'; +process.env.SNYK_INTERNAL_ORGID = '12345678-1234-1234-1234-123456789012'; const apiKey = '123456789'; let oldkey; let oldendpoint; diff --git a/test/tap/severity-threshold.test.ts b/test/tap/severity-threshold.test.ts index 3eaa26d80f..f38954abd9 100644 --- a/test/tap/severity-threshold.test.ts +++ b/test/tap/severity-threshold.test.ts @@ -10,6 +10,7 @@ const BASE_API = '/api/v1'; process.env.SNYK_API = 'http://localhost:' + port + BASE_API; process.env.SNYK_HOST = 'http://localhost:' + port; process.env.LOG_LEVEL = '0'; +process.env.SNYK_INTERNAL_ORGID = '12345678-1234-1234-1234-123456789012'; let oldkey; let oldendpoint; const server = fakeServer(BASE_API, apiKey);