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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/puzpuzpuz/xsync v1.5.2
github.com/rs/zerolog v1.34.0
github.com/snyk/error-catalog-golang-public v0.0.0-20260205094614-116c03822905
github.com/snyk/go-application-framework v0.9.0
github.com/snyk/go-application-framework v0.11.0
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.11.1
golang.org/x/net v0.55.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,8 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/snyk/error-catalog-golang-public v0.0.0-20260205094614-116c03822905 h1:pUe6iOWHEOFY0t4u4ssXeTqpMmZBu1xq06VBFI9zUik=
github.com/snyk/error-catalog-golang-public v0.0.0-20260205094614-116c03822905/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4=
github.com/snyk/go-application-framework v0.9.0 h1:el8tH/5gHLXFVzi63ldQeUxzbnACDvrpP2yLvoGfLQ0=
github.com/snyk/go-application-framework v0.9.0/go.mod h1:0YC7xCETnFTdz6rq8OQPL0aWekMLOkBQu1FE4/cReMA=
github.com/snyk/go-application-framework v0.11.0 h1:lOZhYO8JmzMoZKQbCb/jHMnrB/N3TvX8Z6oE/L9OQ7o=
github.com/snyk/go-application-framework v0.11.0/go.mod h1:9GV/CTAhM8PT9MbxwYt/Za7tKDtw/Wuq6SyCu1XFzvk=
github.com/snyk/go-httpauth v0.0.0-20231117135515-eb445fea7530 h1:s9PHNkL6ueYRiAKNfd8OVxlUOqU3qY0VDbgCD1f6WQY=
github.com/snyk/go-httpauth v0.0.0-20231117135515-eb445fea7530/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
Expand Down
33 changes: 22 additions & 11 deletions internal/commands/code_workflow/native_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"net/http"
"os"
"path"
"path/filepath"
Expand All @@ -19,7 +18,6 @@ import (
"github.com/snyk/code-client-go/sarif"
"github.com/snyk/code-client-go/scan"
"github.com/snyk/error-catalog-golang-public/code"
"github.com/snyk/go-application-framework/pkg/analytics"
"github.com/snyk/go-application-framework/pkg/configuration"
"github.com/snyk/go-application-framework/pkg/instrumentation"
"github.com/snyk/go-application-framework/pkg/local_workflows/content_type"
Expand Down Expand Up @@ -59,7 +57,7 @@ const (
noReport reportType = "no_report"
)

type OptionalAnalysisFunctions func(context.Context, string, func() *http.Client, *zerolog.Logger, configuration.Configuration, ui.UserInterface, analytics.Analytics) (*sarif.SarifResponse, string, *scan.ResultMetaData, error)
type OptionalAnalysisFunctions func(workflow.InvocationContext, string) (*sarif.SarifResponse, string, *scan.ResultMetaData, error)

type ProgressTrackerFactory struct {
userInterface ui.UserInterface
Expand Down Expand Up @@ -130,7 +128,7 @@ func EntryPointNative(invocationCtx workflow.InvocationContext, opts ...Optional
analyzeFnc = opts[0]
}

result, bundleHash, resultMetaData, err := analyzeFnc(invocationCtx.Context(), path, invocationCtx.GetNetworkAccess().GetHttpClient, logger, config, invocationCtx.GetUserInterface(), invocationCtx.GetAnalytics())
result, bundleHash, resultMetaData, err := analyzeFnc(invocationCtx, path)
isNoFilesErr := bundle.IsNoFilesError(err)
if err != nil && !isNoFilesErr {
return nil, err
Expand Down Expand Up @@ -199,7 +197,14 @@ func EntryPointNative(invocationCtx workflow.InvocationContext, opts ...Optional
}

// default function that uses the code-client-go library
func defaultAnalyzeFunction(ctx context.Context, path string, httpClientFunc func() *http.Client, logger *zerolog.Logger, config configuration.Configuration, userInterface ui.UserInterface, analyticsClient analytics.Analytics) (*sarif.SarifResponse, string, *scan.ResultMetaData, error) {
func defaultAnalyzeFunction(invocationCtx workflow.InvocationContext, path string) (*sarif.SarifResponse, string, *scan.ResultMetaData, error) {
ctx := invocationCtx.Context()
config := invocationCtx.GetConfiguration()
logger := invocationCtx.GetEnhancedLogger()
httpClientFunc := invocationCtx.GetNetworkAccess().GetHttpClient
userInterface := invocationCtx.GetUserInterface()
analyticsClient := invocationCtx.GetAnalytics()

var result *sarif.SarifResponse
var resultMetaData *scan.ResultMetaData
requestId, err := uuid.GenerateUUID()
Expand Down Expand Up @@ -285,7 +290,7 @@ func defaultAnalyzeFunction(ctx context.Context, path string, httpClientFunc fun
)
}

target, files, err := determineAnalyzeInput(path, config, logger)
target, files, err := determineAnalyzeInput(invocationCtx, path)
Comment thread
danskmt marked this conversation as resolved.
if err != nil {
return nil, "", nil, err
}
Expand Down Expand Up @@ -347,7 +352,10 @@ func analyzeWithLegacyEngine(
return result, bundleHash, nil, err
}

func determineAnalyzeInput(path string, config configuration.Configuration, logger *zerolog.Logger) (scan.Target, <-chan string, error) {
func determineAnalyzeInput(invocationCtx workflow.InvocationContext, path string) (scan.Target, <-chan string, error) {
config := invocationCtx.GetConfiguration()
logger := invocationCtx.GetEnhancedLogger()

var files <-chan string

pathIsDirectory := false
Expand Down Expand Up @@ -377,17 +385,20 @@ func determineAnalyzeInput(path string, config configuration.Configuration, logg
logger.Warn().Err(err).Msg("could not determine repository URL; consistent-ignores and SCM association may not be applied. Pass --remote-repo-url to set it explicitly")
}

files, err = getFilesForPath(path, logger, config)
files, err = getFilesForPath(invocationCtx, path)
if err != nil {
return nil, nil, err
}

return target, files, nil
}

// Return a channel that notifies each file in the path that doesn't match the filter rules
func getFilesForPath(path string, logger *zerolog.Logger, config configuration.Configuration) (<-chan string, error) {
filter := utils.NewFileFilterFromConfig(path, logger, config, utils.WithThreadNumber(config.GetInt(configuration.MAX_THREADS)))
// Return a channel that notifies each file in the path that doesn't match the filter rules.
// The file filter comes from the invocation context, so it is already wired to the invocation's
// configuration and logger.
func getFilesForPath(invocationCtx workflow.InvocationContext, path string) (<-chan string, error) {
maxThreads := invocationCtx.GetConfiguration().GetInt(configuration.MAX_THREADS)
filter := invocationCtx.GetFileFilter(path, utils.WithThreadNumber(maxThreads))
rules, err := filter.GetRules([]string{".gitignore", ".dcignore", ".snyk"})
if err != nil {
return nil, err
Expand Down
151 changes: 121 additions & 30 deletions internal/commands/code_workflow/native_workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"sync"
"testing"

"github.com/golang/mock/gomock"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
Expand All @@ -24,10 +25,50 @@ import (
"github.com/snyk/code-client-go/scan"
"github.com/snyk/go-application-framework/pkg/analytics"
"github.com/snyk/go-application-framework/pkg/configuration"
gafmocks "github.com/snyk/go-application-framework/pkg/mocks"
"github.com/snyk/go-application-framework/pkg/networking"
"github.com/snyk/go-application-framework/pkg/ui"
"github.com/snyk/go-application-framework/pkg/utils"
"github.com/snyk/go-application-framework/pkg/workflow"
)

// testInvocationContext builds the InvocationContext the analyze function pulls its dependencies
// out of. GetFileFilter mirrors the framework implementation, so file filtering is exercised for
// real rather than stubbed.
func testInvocationContext(
t *testing.T,
config configuration.Configuration,
logger *zerolog.Logger,
httpClientFunc func() *http.Client,
userInterface ui.UserInterface,
analyticsClient analytics.Analytics,
) workflow.InvocationContext {
t.Helper()
ctrl := gomock.NewController(t)

networkAccess := gafmocks.NewMockNetworkAccess(ctrl)
networkAccess.EXPECT().GetHttpClient().DoAndReturn(func() *http.Client {
if httpClientFunc == nil {
return nil
}
return httpClientFunc()
}).AnyTimes()

ictx := gafmocks.NewMockInvocationContext(ctrl)
ictx.EXPECT().Context().Return(context.Background()).AnyTimes()
ictx.EXPECT().GetConfiguration().Return(config).AnyTimes()
ictx.EXPECT().GetEnhancedLogger().Return(logger).AnyTimes()
ictx.EXPECT().GetNetworkAccess().Return(networkAccess).AnyTimes()
ictx.EXPECT().GetUserInterface().Return(userInterface).AnyTimes()
ictx.EXPECT().GetAnalytics().Return(analyticsClient).AnyTimes()
ictx.EXPECT().GetFileFilter(gomock.Any(), gomock.Any()).DoAndReturn(
func(path string, options ...utils.FileFilterOption) *utils.FileFilter {
return utils.NewFileFilter(path, logger, append([]utils.FileFilterOption{utils.WithConfig(config)}, options...)...)
}).AnyTimes()

return ictx
}

func Test_defaultAnalyzeFunction_reportNotSupportedWithSCLE(t *testing.T) {
logger := zerolog.Nop()

Expand All @@ -37,7 +78,9 @@ func Test_defaultAnalyzeFunction_reportNotSupportedWithSCLE(t *testing.T) {
config.Set(ConfigurationProjectName, "my-project") // makes report mode localCode
config.Set(ConfigurationSlceEnabled, true)

_, _, _, err := defaultAnalyzeFunction(context.Background(), t.TempDir(), nil, &logger, config, nil, analytics.New())
ictx := testInvocationContext(t, config, &logger, nil, nil, analytics.New())

_, _, _, err := defaultAnalyzeFunction(ictx, t.TempDir())

assert.Error(t, err)
assert.Contains(t, err.Error(), "Snyk Code Local Engine")
Expand Down Expand Up @@ -119,15 +162,9 @@ func Test_defaultAnalyzeFunction_usesLocalEngineLegacyEndpoints(t *testing.T) {

analyticsClient := analytics.New()

result, actualBundleHash, resultMetaData, err := defaultAnalyzeFunction(
context.Background(),
path,
func() *http.Client { return server.Client() },
&logger,
config,
ui.DefaultUi(),
analyticsClient,
)
ictx := testInvocationContext(t, config, &logger, func() *http.Client { return server.Client() }, ui.DefaultUi(), analyticsClient)

result, actualBundleHash, resultMetaData, err := defaultAnalyzeFunction(ictx, path)

require.NoError(t, err)
require.NotNil(t, result)
Expand Down Expand Up @@ -204,15 +241,9 @@ func Test_defaultAnalyzeFunction_usesFileUploadApi(t *testing.T) {

analyticsClient := analytics.New()

result, _, _, err := defaultAnalyzeFunction(
context.Background(),
path,
func() *http.Client { return server.Client() },
&logger,
config,
ui.DefaultUi(),
analyticsClient,
)
ictx := testInvocationContext(t, config, &logger, func() *http.Client { return server.Client() }, ui.DefaultUi(), analyticsClient)

result, _, _, err := defaultAnalyzeFunction(ictx, path)

require.NoError(t, err)
assert.Nil(t, result)
Expand Down Expand Up @@ -286,15 +317,9 @@ func Test_defaultAnalyzeFunction_recordsFailedFileUploadApiUpload(t *testing.T)

analyticsClient := analytics.New()

result, _, _, err := defaultAnalyzeFunction(
context.Background(),
path,
func() *http.Client { return server.Client() },
&logger,
config,
ui.DefaultUi(),
analyticsClient,
)
ictx := testInvocationContext(t, config, &logger, func() *http.Client { return server.Client() }, ui.DefaultUi(), analyticsClient)

result, _, _, err := defaultAnalyzeFunction(ictx, path)

require.Error(t, err)
assert.Contains(t, err.Error(), "error uploading files")
Expand Down Expand Up @@ -405,7 +430,9 @@ func Test_determineAnalyzeInput(t *testing.T) {
t.Run("given a folder", func(t *testing.T) {
count := 0

target, files, err := determineAnalyzeInput(path, config, &logger)
ictx := testInvocationContext(t, config, &logger, nil, nil, analytics.New())

target, files, err := determineAnalyzeInput(ictx, path)
assert.NoError(t, err)
assert.NotNil(t, target)
assert.NotNil(t, files)
Expand All @@ -422,7 +449,9 @@ func Test_determineAnalyzeInput(t *testing.T) {
t.Run("given a file", func(t *testing.T) {
count := 0

target, files, err := determineAnalyzeInput(filenames[1], config, &logger)
ictx := testInvocationContext(t, config, &logger, nil, nil, analytics.New())

target, files, err := determineAnalyzeInput(ictx, filenames[1])
assert.NoError(t, err)
assert.NotNil(t, target)
assert.NotNil(t, files)
Expand Down Expand Up @@ -459,3 +488,65 @@ func Test_TrackUsage(t *testing.T) {

assert.True(t, trackUsageCalled)
}

// Test_determineAnalyzeInput_usesInvocationContextFileFilter pins that filtering is obtained from
// the invocation context. Constructing a FileFilter directly would still compile and still filter,
// but would silently drop every configuration-gated behavior (the ignore-rule metacharacter fix,
// tracked-file handling), so nothing else in the suite would fail.
func Test_determineAnalyzeInput_usesInvocationContextFileFilter(t *testing.T) {
logger := zerolog.Nop()
config := configuration.NewWithOpts()
config.Set(configuration.MAX_THREADS, 1)

dir := t.TempDir()
writeFile(t, filepath.Join(dir, "app.js"))

ctrl := gomock.NewController(t)
ictx := gafmocks.NewMockInvocationContext(ctrl)
ictx.EXPECT().Context().Return(context.Background()).AnyTimes()
ictx.EXPECT().GetConfiguration().Return(config).AnyTimes()
ictx.EXPECT().GetEnhancedLogger().Return(&logger).AnyTimes()

// exactly once, rooted at the scanned directory
ictx.EXPECT().GetFileFilter(dir, gomock.Any()).Times(1).DoAndReturn(
func(path string, options ...utils.FileFilterOption) *utils.FileFilter {
return utils.NewFileFilter(path, &logger, options...)
})

_, files, err := determineAnalyzeInput(ictx, dir)
require.NoError(t, err)

var found []string
for f := range files {
found = append(found, filepath.Base(f))
}
assert.Equal(t, []string{"app.js"}, found)
}

// Test_defaultAnalyzeFunction_readsDependenciesFromInvocationContext pins that the analysis pulls
// each dependency from the invocation context. EntryPointNative used to extract these and pass them
// in, so this assertion lives here now that the analysis does it itself.
func Test_defaultAnalyzeFunction_readsDependenciesFromInvocationContext(t *testing.T) {
logger := zerolog.Nop()
config := configuration.NewWithOpts()
config.Set(ConfigurationReportFlag, true)
config.Set(ConfigurationProjectName, "my-project")
config.Set(ConfigurationSlceEnabled, true) // errors out after the dependencies are read

ctrl := gomock.NewController(t)
networkAccess := gafmocks.NewMockNetworkAccess(ctrl)
networkAccess.EXPECT().GetHttpClient().Return(nil).AnyTimes()

ictx := gafmocks.NewMockInvocationContext(ctrl)
ictx.EXPECT().Context().Return(context.Background()).Times(1)
ictx.EXPECT().GetNetworkAccess().Return(networkAccess).Times(1)
ictx.EXPECT().GetUserInterface().Return(ui.DefaultUi()).Times(1)
ictx.EXPECT().GetAnalytics().Return(analytics.New()).Times(1)
ictx.EXPECT().GetConfiguration().Return(config).AnyTimes()
ictx.EXPECT().GetEnhancedLogger().Return(&logger).AnyTimes()

result, bundleHash, _, err := defaultAnalyzeFunction(ictx, t.TempDir())
require.Error(t, err)
assert.Nil(t, result)
assert.Empty(t, bundleHash)
}
Loading