Skip to content

Commit 702bbef

Browse files
authored
Add optional git clone timeout (#4597)
We have identified some cases in which it is preferable to time a clone out instead of waiting forever. These situations are unusual, so the CLI option to enable this (which I added for testing) is hidden so that we minimize the risk of baking this option into the interface.
1 parent 83235dd commit 702bbef

File tree

4 files changed

+59
-4
lines changed

4 files changed

+59
-4
lines changed

main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ var (
8888
// Add feature flags
8989
forceSkipBinaries = cli.Flag("force-skip-binaries", "Force skipping binaries.").Bool()
9090
forceSkipArchives = cli.Flag("force-skip-archives", "Force skipping archives.").Bool()
91+
gitCloneTimeout = cli.Flag("git-clone-timeout", "Maximum time to spend cloning a repository, as a duration.").Hidden().Duration()
9192
skipAdditionalRefs = cli.Flag("skip-additional-refs", "Skip additional references.").Bool()
9293
userAgentSuffix = cli.Flag("user-agent-suffix", "Suffix to add to User-Agent.").String()
9394

@@ -451,6 +452,10 @@ func run(state overseer.State) {
451452
feature.ForceSkipArchives.Store(true)
452453
}
453454

455+
if gitCloneTimeout != nil {
456+
feature.GitGloneTimeoutDuration.Store(int64(*gitCloneTimeout))
457+
}
458+
454459
if *skipAdditionalRefs {
455460
feature.SkipAdditionalRefs.Store(true)
456461
}

pkg/feature/feature.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package feature
22

3-
import "sync/atomic"
3+
import (
4+
"sync/atomic"
5+
)
46

57
var (
68
ForceSkipBinaries atomic.Bool
79
ForceSkipArchives atomic.Bool
10+
GitGloneTimeoutDuration atomic.Int64
811
SkipAdditionalRefs atomic.Bool
912
EnableAPKHandler atomic.Bool
1013
UserAgentSuffix AtomicString

pkg/sources/git/git.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ type cloneParams struct {
448448
args []string
449449
clonePath string
450450
authInUrl bool
451+
timeout time.Duration
451452
}
452453

453454
// CloneRepo orchestrates the cloning of a given Git repository, returning its local path
@@ -473,7 +474,9 @@ func CloneRepo(ctx context.Context, userInfo *url.Userinfo, gitURL string, clone
473474
}
474475
}
475476

476-
repo, err := executeClone(ctx, cloneParams{userInfo, gitURL, args, path, authInUrl})
477+
timeout := time.Duration(feature.GitGloneTimeoutDuration.Load())
478+
479+
repo, err := executeClone(ctx, cloneParams{userInfo, gitURL, args, path, authInUrl, timeout})
477480
if err != nil {
478481
// DO NOT FORGET TO CLEAN UP THE CLONE PATH HERE!!
479482
// If we don't, we'll end up with a bunch of orphaned directories in the temp dir.
@@ -529,10 +532,16 @@ func executeClone(ctx context.Context, params cloneParams) (*git.Repository, err
529532
}
530533
}
531534

535+
var cancel context.CancelFunc
536+
if params.timeout > 0 {
537+
ctx, cancel = context.WithTimeout(ctx, params.timeout)
538+
defer cancel()
539+
}
540+
532541
gitArgs = append(gitArgs, "--quiet")
533542
gitArgs = append(gitArgs, params.args...)
534543
gitArgs = append(gitArgs, cloneURL.String(), params.clonePath)
535-
cloneCmd := exec.Command("git", gitArgs...)
544+
cloneCmd := exec.CommandContext(ctx, "git", gitArgs...)
536545

537546
safeURL, secretForRedaction, err := stripPassword(params.gitURL)
538547
if err != nil {
@@ -559,7 +568,9 @@ func executeClone(ctx context.Context, params cloneParams) (*git.Repository, err
559568
}
560569
logger.V(3).Info("git subcommand finished", "output", output)
561570

562-
if cloneCmd.ProcessState == nil {
571+
if common.IsDone(ctx) {
572+
return nil, fmt.Errorf("git clone timed out (after %s)", time.Since(start))
573+
} else if cloneCmd.ProcessState == nil {
563574
return nil, fmt.Errorf("clone command exited with no output")
564575
} else if cloneCmd.ProcessState.ExitCode() != 0 {
565576
logger.V(1).Info("git clone failed", "error", err)

pkg/sources/git/git_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import (
88
"path/filepath"
99
"strings"
1010
"testing"
11+
"time"
1112

1213
"github.com/kylelemons/godebug/pretty"
1314
"github.com/stretchr/testify/assert"
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
1416
"google.golang.org/protobuf/types/known/anypb"
1517

1618
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
@@ -23,6 +25,40 @@ import (
2325
"github.com/trufflesecurity/trufflehog/v3/pkg/sourcestest"
2426
)
2527

28+
func TestClone_Timeout(t *testing.T) {
29+
ctx := context.Background()
30+
31+
t.Run("an unset timeout should not cause a timeout", func(t *testing.T) {
32+
_, _, err := CloneRepo(
33+
ctx,
34+
nil,
35+
"https://github.com/dustin-decker/secretsandstuff.git",
36+
"",
37+
false)
38+
39+
// There shouldn't be an error - but if there is because of some other problem, we don't want this test to fail
40+
if err != nil {
41+
assert.NotContains(t, err.Error(), "timed out")
42+
}
43+
})
44+
45+
t.Run("a clone that times out should time out", func(t *testing.T) {
46+
feature.GitGloneTimeoutDuration.Store(int64(1 * time.Nanosecond))
47+
t.Cleanup(func() { feature.GitGloneTimeoutDuration.Store(0) })
48+
49+
_, _, err := CloneRepo(
50+
ctx,
51+
nil,
52+
"https://github.com/dustin-decker/secretsandstuff.git",
53+
"",
54+
false)
55+
56+
if assert.Error(t, err) {
57+
assert.Contains(t, err.Error(), "timed out")
58+
}
59+
})
60+
}
61+
2662
func TestSource_Scan(t *testing.T) {
2763
ctx, cancel := context.WithCancel(context.Background())
2864
defer cancel()

0 commit comments

Comments
 (0)