Skip to content

Commit 35bab8f

Browse files
dmjbevankanderson
andauthored
Merge pull request from GHSA-hpcg-xjq5-g666
* Add memory consumption limits for memfs * Clean up mix of per-file and total size code * Use memboxfs for object storage * Wire in git fetch limits, still no tests * improve error handling * cover git clone failure case * delete unused file * more tests * remove comment --------- Co-authored-by: Evan Anderson <evan@stacklok.com>
1 parent bb173c2 commit 35bab8f

File tree

20 files changed

+402
-26
lines changed

20 files changed

+402
-26
lines changed

Diff for: cmd/dev/app/image/cmd_verify.go

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func runCmdVerify(cmd *cobra.Command, _ []string) error {
112112
func buildGitHubClient(token string) (provifv1.GitHub, error) {
113113
return clients.NewRestClient(
114114
&minderv1.GitHubProviderConfig{},
115+
nil,
115116
&ratecache.NoopRestClientCache{},
116117
credentials.NewGitHubTokenCredential(token),
117118
clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),

Diff for: cmd/dev/app/rule_type/rttst.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,9 @@ func getProvider(pstr string, token string, providerConfigFile string) (provifv1
302302
case "github":
303303
client, err := clients.NewGitHubAppProvider(
304304
&minderv1.GitHubAppProviderConfig{},
305-
&serverconfig.GitHubAppConfig{AppName: "test"},
305+
&serverconfig.ProviderConfig{
306+
GitHubApp: &serverconfig.GitHubAppConfig{AppName: "test"},
307+
},
306308
&ratecache.NoopRestClientCache{},
307309
credentials.NewGitHubTokenCredential(token),
308310
nil,

Diff for: internal/config/server/provider.go

+6
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,10 @@ package server
1919
type ProviderConfig struct {
2020
GitHubApp *GitHubAppConfig `mapstructure:"github-app"`
2121
GitHub *GitHubConfig `mapstructure:"github"`
22+
Git GitConfig `mapstructure:"git"`
23+
}
24+
25+
type GitConfig struct {
26+
MaxFiles int64 `mapstructure:"max_files" default:"10000"`
27+
MaxBytes int64 `mapstructure:"max_bytes" default:"100_000_000"`
2228
}

Diff for: internal/config/utils.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ func getDefaultValueForInt64(value string) (any, error) {
258258
var defaultValue any
259259
var err error
260260

261-
defaultValue, err = strconv.Atoi(value)
261+
defaultValue, err = strconv.ParseInt(value, 0, 0)
262262
if err == nil {
263263
return defaultValue, nil
264264
}
@@ -285,7 +285,7 @@ func getDefaultValue(field reflect.StructField, value string, valueName string)
285285
defaultValue, err = getDefaultValueForInt64(value)
286286
case reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int,
287287
reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint:
288-
defaultValue, err = strconv.Atoi(value)
288+
defaultValue, err = strconv.ParseInt(value, 0, 0)
289289
case reflect.Float64:
290290
defaultValue, err = strconv.ParseFloat(value, 64)
291291
case reflect.Bool:

Diff for: internal/engine/actions/remediate/gh_branch_protect/gh_branch_protect_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func testGithubProvider(baseURL string) (provifv1.GitHub, error) {
5656
&pb.GitHubProviderConfig{
5757
Endpoint: baseURL,
5858
},
59+
nil,
5960
&ratecache.NoopRestClientCache{},
6061
credentials.NewGitHubTokenCredential("token"),
6162
clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),

Diff for: internal/engine/actions/remediate/pull_request/pull_request_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ func testGithubProvider() (provifv1.GitHub, error) {
9393
&pb.GitHubProviderConfig{
9494
Endpoint: ghApiUrl + "/",
9595
},
96+
nil,
9697
&ratecache.NoopRestClientCache{},
9798
credentials.NewGitHubTokenCredential("token"),
9899
clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),

Diff for: internal/engine/actions/remediate/rest/rest_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func testGithubProvider(baseURL string) (provifv1.REST, error) {
5656
&pb.GitHubProviderConfig{
5757
Endpoint: baseURL,
5858
},
59+
nil,
5960
&ratecache.NoopRestClientCache{},
6061
credentials.NewGitHubTokenCredential("token"),
6162
clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),

Diff for: internal/engine/ingester/artifact/artifact_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func testGithubProvider() (provinfv1.GitHub, error) {
4848
&pb.GitHubProviderConfig{
4949
Endpoint: baseURL,
5050
},
51+
nil,
5152
&ratecache.NoopRestClientCache{},
5253
credentials.NewGitHubTokenCredential("token"),
5354
clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),

Diff for: internal/engine/ingester/git/git_test.go

+68-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ package git_test
1717
import (
1818
"bytes"
1919
"context"
20+
"github.com/stacklok/minder/internal/config/server"
21+
v1 "github.com/stacklok/minder/pkg/providers/v1"
2022
"testing"
2123

2224
"github.com/stretchr/testify/require"
@@ -31,9 +33,18 @@ import (
3133
func TestGitIngestWithCloneURLFromRepo(t *testing.T) {
3234
t.Parallel()
3335

36+
cfg := server.GitConfig{
37+
MaxFiles: 100,
38+
MaxBytes: 1_000_000,
39+
}
40+
gitClient := gitclient.NewGit(
41+
credentials.NewEmptyCredential(),
42+
gitclient.WithConfig(cfg),
43+
)
44+
3445
gi, err := gitengine.NewGitIngester(
3546
&pb.GitType{Branch: "master"},
36-
gitclient.NewGit(credentials.NewEmptyCredential()),
47+
gitClient,
3748
)
3849
require.NoError(t, err, "expected no error")
3950

@@ -193,3 +204,59 @@ func TestGitIngestFailsBecauseOfUnexistentCloneUrl(t *testing.T) {
193204
require.Error(t, err, "expected error")
194205
require.Nil(t, got, "expected nil result")
195206
}
207+
208+
func TestGitIngestFailsWhenRepoTooLarge(t *testing.T) {
209+
t.Parallel()
210+
211+
// set size limit to 1 byte
212+
cfg := server.GitConfig{
213+
MaxFiles: 100,
214+
MaxBytes: 1,
215+
}
216+
gitClient := gitclient.NewGit(
217+
credentials.NewEmptyCredential(),
218+
gitclient.WithConfig(cfg),
219+
)
220+
221+
gi, err := gitengine.NewGitIngester(
222+
&pb.GitType{Branch: "master"},
223+
gitClient,
224+
)
225+
226+
require.NoError(t, err, "expected no error")
227+
228+
got, err := gi.Ingest(context.Background(), &pb.Artifact{}, map[string]any{
229+
"clone_url": "https://github.com/octocat/Hello-World.git",
230+
})
231+
require.Error(t, err, "expected error")
232+
require.ErrorIs(t, err, v1.ErrRepositoryTooLarge, "expected ErrRepositoryTooLarge")
233+
require.Nil(t, got, "expected non-nil result")
234+
}
235+
236+
func TestGitIngestFailsWhenRepoHasTooManyFiles(t *testing.T) {
237+
t.Parallel()
238+
239+
// will fail because of files in .git
240+
cfg := server.GitConfig{
241+
MaxFiles: 1,
242+
MaxBytes: 1_000_000,
243+
}
244+
gitClient := gitclient.NewGit(
245+
credentials.NewEmptyCredential(),
246+
gitclient.WithConfig(cfg),
247+
)
248+
249+
gi, err := gitengine.NewGitIngester(
250+
&pb.GitType{Branch: "master"},
251+
gitClient,
252+
)
253+
254+
require.NoError(t, err, "expected no error")
255+
256+
got, err := gi.Ingest(context.Background(), &pb.Artifact{}, map[string]any{
257+
"clone_url": "https://github.com/octocat/Hello-World.git",
258+
})
259+
require.Error(t, err, "expected error")
260+
require.ErrorIs(t, err, v1.ErrRepositoryTooLarge, "expected ErrRepositoryTooLarge")
261+
require.Nil(t, got, "expected non-nil result")
262+
}

Diff for: internal/engine/ingester/ingester_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ func TestNewRuleDataIngest(t *testing.T) {
160160

161161
client, err := clients.NewRestClient(
162162
&pb.GitHubProviderConfig{},
163+
nil,
163164
&ratecache.NoopRestClientCache{},
164165
credentials.NewGitHubTokenCredential("token"),
165166
clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),

Diff for: internal/engine/ingester/rest/rest_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ func testGithubProviderBuilder(baseURL string) (provifv1.REST, error) {
114114
&pb.GitHubProviderConfig{
115115
Endpoint: baseURL,
116116
},
117+
nil,
117118
&ratecache.NoopRestClientCache{},
118119
credentials.NewGitHubTokenCredential("token"),
119120
clients.NewGitHubClientFactory(telemetry.NewNoopMetrics()),

Diff for: internal/providers/git/git.go

+40-7
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,51 @@ import (
1919
"context"
2020
"errors"
2121
"fmt"
22+
"io/fs"
2223

2324
"github.com/go-git/go-billy/v5/memfs"
25+
"github.com/go-git/go-git/v5/plumbing/cache"
26+
"github.com/go-git/go-git/v5/storage/filesystem"
27+
2428
"github.com/go-git/go-git/v5"
2529
"github.com/go-git/go-git/v5/plumbing"
2630
"github.com/go-git/go-git/v5/plumbing/transport"
27-
"github.com/go-git/go-git/v5/storage/memory"
28-
31+
"github.com/stacklok/minder/internal/config/server"
32+
"github.com/stacklok/minder/internal/providers/git/memboxfs"
2933
minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
3034
provifv1 "github.com/stacklok/minder/pkg/providers/v1"
3135
)
3236

3337
// Git is the struct that contains the GitHub REST API client
3438
type Git struct {
3539
credential provifv1.GitCredential
40+
maxFiles int64
41+
maxBytes int64
3642
}
3743

44+
const maxCachedObjectSize = 100 * 1024 // 100KiB
45+
3846
// Ensure that the Git client implements the Git interface
3947
var _ provifv1.Git = (*Git)(nil)
4048

49+
type Options func(*Git)
50+
4151
// NewGit creates a new GitHub client
42-
func NewGit(token provifv1.GitCredential) *Git {
43-
return &Git{
52+
func NewGit(token provifv1.GitCredential, opts ...Options) *Git {
53+
ret := &Git{
4454
credential: token,
4555
}
56+
for _, opt := range opts {
57+
opt(ret)
58+
}
59+
return ret
60+
}
61+
62+
func WithConfig(cfg server.GitConfig) Options {
63+
return func(g *Git) {
64+
g.maxFiles = cfg.MaxFiles
65+
g.maxBytes = cfg.MaxBytes
66+
}
4667
}
4768

4869
// CanImplement returns true/false depending on whether the Provider
@@ -67,20 +88,32 @@ func (g *Git) Clone(ctx context.Context, url, branch string) (*git.Repository, e
6788
return nil, fmt.Errorf("invalid clone options: %w", err)
6889
}
6990

70-
storer := memory.NewStorage()
71-
fs := memfs.New()
91+
// TODO(#3582): Switch this to use a tmpfs backed clone
92+
memFS := memfs.New()
93+
if g.maxFiles != 0 && g.maxBytes != 0 {
94+
memFS = &memboxfs.LimitedFs{
95+
Fs: memFS,
96+
MaxFiles: g.maxFiles,
97+
TotalFileSize: g.maxBytes,
98+
}
99+
}
100+
storerCache := cache.NewObjectLRU(maxCachedObjectSize)
101+
// reuse same FS for storage and cloned files
102+
storer := filesystem.NewStorage(memFS, storerCache)
72103

73104
// We clone to the memfs go-billy filesystem driver, which doesn't
74105
// allow for direct access to the underlying filesystem. This is
75106
// because we want to be able to run this in a sandboxed environment
76107
// where we don't have access to the underlying filesystem.
77-
r, err := git.CloneContext(ctx, storer, fs, opts)
108+
r, err := git.CloneContext(ctx, storer, memFS, opts)
78109
if err != nil {
79110
var refspecerr git.NoMatchingRefSpecError
80111
if errors.Is(err, git.ErrBranchNotFound) || refspecerr.Is(err) {
81112
return nil, provifv1.ErrProviderGitBranchNotFound
82113
} else if errors.Is(err, transport.ErrEmptyRemoteRepository) {
83114
return nil, provifv1.ErrRepositoryEmpty
115+
} else if errors.Is(err, fs.ErrPermission) {
116+
return nil, provifv1.ErrRepositoryTooLarge
84117
}
85118
return nil, fmt.Errorf("could not clone repo: %w", err)
86119
}

0 commit comments

Comments
 (0)