Skip to content
This repository was archived by the owner on May 28, 2026. It is now read-only.

Commit 07c1663

Browse files
Enable cloning repository to a specified location with retention option (trufflesecurity#4408)
* Enabled cloning repositories to a specified path with retention option * Fixes after testing * resolved lint issue * resolved comments * enabled clone path for github basic auth
1 parent c683e0a commit 07c1663

21 files changed

Lines changed: 819 additions & 606 deletions

File tree

hack/snifftest/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ func main() {
181181
defer sem.Release(1)
182182
defer wgChunkers.Done()
183183
logger.Info("cloning repo", "repo", r)
184-
path, repo, err := git.CloneRepoUsingUnauthenticated(ctx, r)
184+
path, repo, err := git.CloneRepoUsingUnauthenticated(ctx, r, "")
185185
if err != nil {
186186
logFatal(err, "error cloning repo", "repo", r)
187187
}

main.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ var (
9999
gitScanBranch = gitScan.Flag("branch", "Branch to scan.").String()
100100
gitScanMaxDepth = gitScan.Flag("max-depth", "Maximum depth of commits to scan.").Int()
101101
gitScanBare = gitScan.Flag("bare", "Scan bare repository (e.g. useful while using in pre-receive hooks)").Bool()
102+
gitClonePath = gitScan.Flag("clone-path", "Custom path where the repository should be cloned (default: temp dir).").String()
103+
gitNoCleanup = gitScan.Flag("no-cleanup", "Do not delete cloned repositories after scanning (can only be used with --clone-path).").Bool()
102104
_ = gitScan.Flag("allow", "No-op flag for backwards compat.").Bool()
103105
_ = gitScan.Flag("entropy", "No-op flag for backwards compat.").Bool()
104106
_ = gitScan.Flag("regex", "No-op flag for backwards compat.").Bool()
@@ -120,6 +122,8 @@ var (
120122
githubScanGistComments = githubScan.Flag("gist-comments", "Include gist comments in scan.").Bool()
121123
githubCommentsTimeframeDays = githubScan.Flag("comments-timeframe", "Number of days in the past to review when scanning issue, PR, and gist comments.").Uint32()
122124
githubAuthInUrl = githubScan.Flag("auth-in-url", "Embed authentication credentials in repository URLs instead of using secure HTTP headers").Bool()
125+
githubClonePath = githubScan.Flag("clone-path", "Custom path where the repository should be cloned (default: temp dir).").String()
126+
githubNoCleanup = githubScan.Flag("no-cleanup", "Do not delete cloned repositories after scanning (can only be used with --clone-path).").Bool()
123127

124128
// GitHub Cross Fork Object Reference Experimental Feature
125129
githubExperimentalScan = cli.Command("github-experimental", "Run an experimental GitHub scan. Must specify at least one experimental sub-module to run: object-discovery.")
@@ -142,6 +146,8 @@ var (
142146
gitlabScanIncludeRepos = gitlabScan.Flag("include-repos", `Repositories to include in an org scan. This can also be a glob pattern. You can repeat this flag. Must use Gitlab repo full name. Example: "trufflesecurity/trufflehog", "trufflesecurity/t*"`).Strings()
143147
gitlabScanExcludeRepos = gitlabScan.Flag("exclude-repos", `Repositories to exclude in an org scan. This can also be a glob pattern. You can repeat this flag. Must use Gitlab repo full name. Example: "trufflesecurity/driftwood", "trufflesecurity/d*"`).Strings()
144148
gitlabAuthInUrl = gitlabScan.Flag("auth-in-url", "Embed authentication credentials in repository URLs instead of using secure HTTP headers").Bool()
149+
gitlabClonePath = gitlabScan.Flag("clone-path", "Custom path where the repository should be cloned (default: temp dir)").String()
150+
gitlabNoCleanup = gitlabScan.Flag("no-cleanup", "Do not delete cloned repositories after scanning (can only be used with --clone-path).").Bool()
145151

146152
filesystemScan = cli.Command("filesystem", "Find credentials in a filesystem.")
147153
filesystemPaths = filesystemScan.Arg("path", "Path to file or directory to scan.").Strings()
@@ -719,6 +725,15 @@ func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics,
719725
}
720726
}
721727

728+
// clone path is only supported for HTTPS repository URLs, not for local repositories.
729+
if *gitClonePath != "" && strings.HasPrefix(*gitScanURI, "file://") {
730+
return scanMetrics, fmt.Errorf("invalid configuration: --clone-path cannot be used with a local repository URL")
731+
}
732+
733+
if err := validateClonePath(*gitClonePath, *gitNoCleanup); err != nil {
734+
return scanMetrics, err
735+
}
736+
722737
gitCfg := sources.GitConfig{
723738
URI: *gitScanURI,
724739
IncludePathsFile: *gitScanIncludePaths,
@@ -728,6 +743,8 @@ func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics,
728743
MaxDepth: *gitScanMaxDepth,
729744
Bare: *gitScanBare,
730745
ExcludeGlobs: *gitScanExcludeGlobs,
746+
ClonePath: *gitClonePath,
747+
NoCleanup: *gitNoCleanup,
731748
}
732749
if ref, err := eng.ScanGit(ctx, gitCfg); err != nil {
733750
return scanMetrics, fmt.Errorf("failed to scan Git: %v", err)
@@ -746,6 +763,10 @@ func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics,
746763
return scanMetrics, fmt.Errorf("invalid config: you cannot specify both organizations and repositories at the same time")
747764
}
748765

766+
if err := validateClonePath(*githubClonePath, *githubNoCleanup); err != nil {
767+
return scanMetrics, err
768+
}
769+
749770
cfg := sources.GithubConfig{
750771
Endpoint: *githubScanEndpoint,
751772
Token: *githubScanToken,
@@ -763,7 +784,10 @@ func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics,
763784
CommentsTimeframeDays: *githubCommentsTimeframeDays,
764785
Filter: filter,
765786
AuthInUrl: *githubAuthInUrl,
787+
ClonePath: *githubClonePath,
788+
NoCleanup: *githubNoCleanup,
766789
}
790+
767791
if ref, err := eng.ScanGitHub(ctx, cfg); err != nil {
768792
return scanMetrics, fmt.Errorf("failed to scan Github: %v", err)
769793
} else {
@@ -792,6 +816,10 @@ func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics,
792816
return scanMetrics, fmt.Errorf("invalid config: you cannot specify both repositories and groups at the same time")
793817
}
794818

819+
if err := validateClonePath(*gitlabClonePath, *gitlabNoCleanup); err != nil {
820+
return scanMetrics, err
821+
}
822+
795823
cfg := sources.GitlabConfig{
796824
Endpoint: *gitlabScanEndpoint,
797825
Token: *gitlabScanToken,
@@ -801,7 +829,10 @@ func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics,
801829
ExcludeRepos: *gitlabScanExcludeRepos,
802830
Filter: filter,
803831
AuthInUrl: *gitlabAuthInUrl,
832+
ClonePath: *gitlabClonePath,
833+
NoCleanup: *gitlabNoCleanup,
804834
}
835+
805836
if ref, err := eng.ScanGitLab(ctx, cfg); err != nil {
806837
return scanMetrics, fmt.Errorf("failed to scan GitLab: %v", err)
807838
} else {
@@ -1120,3 +1151,31 @@ func isValidCommit(uri, commit string) bool {
11201151

11211152
return strings.TrimSpace(string(output)) == "commit"
11221153
}
1154+
1155+
// validateClonePath ensures that --clone-path, if provided, exists and is a directory.
1156+
// It also verifies that --no-cleanup is only allowed when --clone-path is set.
1157+
// Note: without a custom clone path, repositories are cloned into temporary directories, which should never be retained.
1158+
func validateClonePath(clonePath string, noCleanup bool) error {
1159+
if noCleanup && clonePath == "" {
1160+
return fmt.Errorf("invalid configuration: --no-cleanup can only be used together with --clone-path")
1161+
}
1162+
1163+
if clonePath == "" {
1164+
return nil
1165+
}
1166+
1167+
info, err := os.Stat(clonePath)
1168+
if err != nil {
1169+
if os.IsNotExist(err) {
1170+
return fmt.Errorf("path provided to --clone-path: %q does not exist", clonePath)
1171+
}
1172+
1173+
return fmt.Errorf("failed to access --clone-path %q: %w", clonePath, err)
1174+
}
1175+
1176+
if !info.IsDir() {
1177+
return fmt.Errorf("path provided to --clone-path: %q is not a directory", clonePath)
1178+
}
1179+
1180+
return nil
1181+
}

pkg/engine/git.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ func (e *Engine) ScanGit(ctx context.Context, c sources.GitConfig) (sources.JobP
2424
ExcludePathsFile: c.ExcludePathsFile,
2525
MaxDepth: int64(c.MaxDepth),
2626
SkipBinaries: c.SkipBinaries,
27+
ClonePath: c.ClonePath,
28+
NoCleanup: c.NoCleanup,
2729
}
30+
2831
var conn anypb.Any
2932
if err := anypb.MarshalFrom(&conn, connection, proto.MarshalOptions{}); err != nil {
3033
ctx.Logger().Error(err, "failed to marshal git connection")

pkg/engine/git_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func (p *discardPrinter) Print(context.Context, *detectors.ResultWithMetadata) e
3232
func TestGitEngine(t *testing.T) {
3333
ctx := context.Background()
3434
repoUrl := "https://github.com/dustin-decker/secretsandstuff.git"
35-
path, _, err := git.PrepareRepo(ctx, repoUrl)
35+
path, _, err := git.PrepareRepo(ctx, repoUrl, "")
3636
if err != nil {
3737
t.Error(err)
3838
}

pkg/engine/github.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ func (e *Engine) ScanGitHub(ctx context.Context, c sources.GithubConfig) (source
2929
SkipBinaries: c.SkipBinaries,
3030
CommentsTimeframeDays: c.CommentsTimeframeDays,
3131
RemoveAuthInUrl: !c.AuthInUrl, // configuration uses the opposite field in proto to keep credentials in the URL by default.
32+
ClonePath: c.ClonePath,
33+
NoCleanup: c.NoCleanup,
3234
}
35+
3336
if len(c.Token) > 0 {
3437
connection.Credential = &sourcespb.GitHub_Token{
3538
Token: c.Token,

pkg/engine/gitlab.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ func (e *Engine) ScanGitLab(ctx context.Context, c sources.GitlabConfig) (source
5858
connection.IgnoreRepos = c.ExcludeRepos
5959
}
6060

61+
if c.ClonePath != "" {
62+
connection.ClonePath = c.ClonePath
63+
}
64+
65+
connection.NoCleanup = c.NoCleanup
66+
6167
var conn anypb.Any
6268
err := anypb.MarshalFrom(&conn, connection, proto.MarshalOptions{})
6369
if err != nil {

pkg/output/legacy_json.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func (p *LegacyJSONPrinter) Print(ctx context.Context, r *detectors.ResultWithMe
3838
}
3939

4040
// cloning the repo again here is not great and only works with unauthed repos
41-
repoPath, remote, err := git.PrepareRepo(ctx, repo)
41+
repoPath, remote, err := git.PrepareRepo(ctx, repo, "")
4242
if err != nil || repoPath == "" {
4343
return fmt.Errorf("error preparing git repo for scanning: %w", err)
4444
}

0 commit comments

Comments
 (0)