Skip to content

Commit 4321561

Browse files
Merge branch 'main' into feat/rootlywebhook-detector
2 parents 5b735e3 + 7d61a4b commit 4321561

File tree

17 files changed

+1797
-896
lines changed

17 files changed

+1797
-896
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ Run trufflehog from the parent directory (outside the git repo).
269269
$ trufflehog git file://test_keys --results=verified,unknown
270270
```
271271

272+
To guard against malicious git configs in local scanning (see CVE-2025-41390), TruffleHog clones local git repositories to a temporary directory prior to scanning. This follows [Git's security best practices](https://git-scm.com/docs/git#_security). If you want to specify a custom path to clone the repository to (instead of tmp), you can use the `--clone-path` flag. If you'd like to skip the local cloning process and scan the repository directly (only do this for trusted repos), you can use the `--trust-local-git-config` flag.
273+
272274
## 10: Scan GCS buckets for only verified secrets
273275

274276
```bash

go.mod

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,6 @@ require (
212212
github.com/golang-sql/sqlexp v0.1.0 // indirect
213213
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
214214
github.com/golang/snappy v1.0.0 // indirect
215-
github.com/google/go-github/v69 v69.0.0 // indirect
216215
github.com/google/go-github/v72 v72.0.0 // indirect
217216
github.com/google/go-querystring v1.1.0 // indirect
218217
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
@@ -253,8 +252,7 @@ require (
253252
github.com/muesli/cancelreader v0.2.2 // indirect
254253
github.com/muesli/termenv v0.16.0 // indirect
255254
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
256-
github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78 // indirect
257-
github.com/olekukonko/tablewriter v0.0.5 // indirect
255+
github.com/nwaples/rardecode/v2 v2.2.1 // indirect
258256
github.com/onsi/ginkgo v1.16.5 // indirect
259257
github.com/opencontainers/go-digest v1.0.0 // indirect
260258
github.com/opencontainers/image-spec v1.1.1 // indirect

go.sum

Lines changed: 12 additions & 166 deletions
Large diffs are not rendered by default.

main.go

Lines changed: 27 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -91,20 +91,21 @@ var (
9191
skipAdditionalRefs = cli.Flag("skip-additional-refs", "Skip additional references.").Bool()
9292
userAgentSuffix = cli.Flag("user-agent-suffix", "Suffix to add to User-Agent.").String()
9393

94-
gitScan = cli.Command("git", "Find credentials in git repositories.")
95-
gitScanURI = gitScan.Arg("uri", "Git repository URL. https://, file://, or ssh:// schema expected.").Required().String()
96-
gitScanIncludePaths = gitScan.Flag("include-paths", "Path to file with newline separated regexes for files to include in scan.").Short('i').String()
97-
gitScanExcludePaths = gitScan.Flag("exclude-paths", "Path to file with newline separated regexes for files to exclude in scan.").Short('x').String()
98-
gitScanExcludeGlobs = gitScan.Flag("exclude-globs", "Comma separated list of globs to exclude in scan. This option filters at the `git log` level, resulting in faster scans.").String()
99-
gitScanSinceCommit = gitScan.Flag("since-commit", "Commit to start scan from.").String()
100-
gitScanBranch = gitScan.Flag("branch", "Branch to scan.").String()
101-
gitScanMaxDepth = gitScan.Flag("max-depth", "Maximum depth of commits to scan.").Int()
102-
gitScanBare = gitScan.Flag("bare", "Scan bare repository (e.g. useful while using in pre-receive hooks)").Bool()
103-
gitClonePath = gitScan.Flag("clone-path", "Custom path where the repository should be cloned (default: temp dir).").String()
104-
gitNoCleanup = gitScan.Flag("no-cleanup", "Do not delete cloned repositories after scanning (can only be used with --clone-path).").Bool()
105-
_ = gitScan.Flag("allow", "No-op flag for backwards compat.").Bool()
106-
_ = gitScan.Flag("entropy", "No-op flag for backwards compat.").Bool()
107-
_ = gitScan.Flag("regex", "No-op flag for backwards compat.").Bool()
94+
gitScan = cli.Command("git", "Find credentials in git repositories.")
95+
gitScanURI = gitScan.Arg("uri", "Git repository URL. https://, file://, or ssh:// schema expected.").Required().String()
96+
gitScanIncludePaths = gitScan.Flag("include-paths", "Path to file with newline separated regexes for files to include in scan.").Short('i').String()
97+
gitScanExcludePaths = gitScan.Flag("exclude-paths", "Path to file with newline separated regexes for files to exclude in scan.").Short('x').String()
98+
gitScanExcludeGlobs = gitScan.Flag("exclude-globs", "Comma separated list of globs to exclude in scan. This option filters at the `git log` level, resulting in faster scans.").String()
99+
gitScanSinceCommit = gitScan.Flag("since-commit", "Commit to start scan from.").String()
100+
gitScanBranch = gitScan.Flag("branch", "Branch to scan.").String()
101+
gitScanMaxDepth = gitScan.Flag("max-depth", "Maximum depth of commits to scan.").Int()
102+
gitScanBare = gitScan.Flag("bare", "Scan bare repository (e.g. useful while using in pre-receive hooks)").Bool()
103+
gitClonePath = gitScan.Flag("clone-path", "Custom path where the repository should be cloned (default: temp dir).").String()
104+
gitNoCleanup = gitScan.Flag("no-cleanup", "Do not delete cloned repositories after scanning (can only be used with --clone-path).").Bool()
105+
gitTrustLocalGitConfig = gitScan.Flag("trust-local-git-config", "Trust local git config.").Bool()
106+
_ = gitScan.Flag("allow", "No-op flag for backwards compat.").Bool()
107+
_ = gitScan.Flag("entropy", "No-op flag for backwards compat.").Bool()
108+
_ = gitScan.Flag("regex", "No-op flag for backwards compat.").Bool()
108109

109110
githubScan = cli.Command("github", "Find credentials in GitHub repositories.")
110111
githubScanEndpoint = githubScan.Flag("endpoint", "GitHub endpoint.").Default("https://api.github.com").String()
@@ -735,35 +736,24 @@ func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics,
735736
var refs []sources.JobProgressRef
736737
switch cmd {
737738
case gitScan.FullCommand():
738-
gitCloneTempPath = *gitClonePath
739-
// validate the commit for local repository only
740-
if *gitScanSinceCommit != "" && strings.HasPrefix(*gitScanURI, "file") {
741-
if !isValidCommit(*gitScanURI, *gitScanSinceCommit) {
742-
ctx.Logger().Info("Warning: The provided commit hash appears to be invalid.")
743-
}
744-
}
745-
746-
// clone path is only supported for HTTPS repository URLs, not for local repositories.
747-
if *gitClonePath != "" && strings.HasPrefix(*gitScanURI, "file://") {
748-
return scanMetrics, fmt.Errorf("invalid configuration: --clone-path cannot be used with a local repository URL")
749-
}
750739

751740
if err := validateClonePath(*gitClonePath, *gitNoCleanup); err != nil {
752741
return scanMetrics, err
753742
}
754743

755744
gitCfg := sources.GitConfig{
756-
URI: *gitScanURI,
757-
IncludePathsFile: *gitScanIncludePaths,
758-
ExcludePathsFile: *gitScanExcludePaths,
759-
HeadRef: *gitScanBranch,
760-
BaseRef: *gitScanSinceCommit,
761-
MaxDepth: *gitScanMaxDepth,
762-
Bare: *gitScanBare,
763-
ExcludeGlobs: *gitScanExcludeGlobs,
764-
ClonePath: *gitClonePath,
765-
NoCleanup: *gitNoCleanup,
766-
PrintLegacyJSON: *jsonLegacy,
745+
URI: *gitScanURI,
746+
IncludePathsFile: *gitScanIncludePaths,
747+
ExcludePathsFile: *gitScanExcludePaths,
748+
HeadRef: *gitScanBranch,
749+
BaseRef: *gitScanSinceCommit,
750+
MaxDepth: *gitScanMaxDepth,
751+
Bare: *gitScanBare,
752+
ExcludeGlobs: *gitScanExcludeGlobs,
753+
ClonePath: *gitClonePath,
754+
NoCleanup: *gitNoCleanup,
755+
PrintLegacyJSON: *jsonLegacy,
756+
TrustLocalGitConfig: *gitTrustLocalGitConfig,
767757
}
768758
if ref, err := eng.ScanGit(ctx, gitCfg); err != nil {
769759
return scanMetrics, fmt.Errorf("failed to scan Git: %v", err)
@@ -1168,18 +1158,6 @@ func printAverageDetectorTime(e *engine.Engine) {
11681158
}
11691159
}
11701160

1171-
// Function to check if the commit is valid
1172-
func isValidCommit(uri, commit string) bool {
1173-
// handle file:// urls
1174-
repoPath, _ := strings.CutPrefix(uri, "file://") // remove the prefix to validate against the repo path
1175-
output, err := exec.Command("git", "-C", repoPath, "cat-file", "-t", commit).Output()
1176-
if err != nil {
1177-
return false
1178-
}
1179-
1180-
return strings.TrimSpace(string(output)) == "commit"
1181-
}
1182-
11831161
// validateClonePath ensures that --clone-path, if provided, exists and is a directory.
11841162
// It also verifies that --no-cleanup is only allowed when --clone-path is set.
11851163
// Note: without a custom clone path, repositories are cloned into temporary directories, which should never be retained.

pkg/detectors/github/v1/github_old.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@ func (Scanner) CloudEndpoint() string { return "https://api.github.com" }
2828
var (
2929
// Oauth token
3030
// https://developer.github.com/v3/#oauth2-token-sent-in-a-header
31-
// the middle regex `(?:[a-zA-Z0-9.\/?=&:-]{0,40})` is to match the prefix of token match to avoid processing common known patterns
32-
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"github", "gh", "pat", "token"}) + `\b(?:[a-zA-Z0-9.\/?=&:-]{0,40})([a-f0-9]{40})\b`)
33-
34-
// TODO: Oauth2 client_id and client_secret
35-
// https://developer.github.com/v3/#oauth2-keysecret
31+
keyPat = regexp.MustCompile(
32+
detectors.PrefixRegex([]string{
33+
"github_token", "github_secret", "github_key", "github_api", "github_pat",
34+
"githubtoken", "githubsecret", "githubkey", "githubapi", "githubpat",
35+
"gh_token", "gh_secret", "gh_key", "gh_api", "gh_pat",
36+
"ghtoken", "ghsecret", "ghkey", "ghapi", "ghpat",
37+
}) + `\b([0-9a-f]{40})\b`,
38+
)
3639
)
3740

3841
// TODO: Add secret context?? Information about access, ownership etc

pkg/engine/git.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,19 @@ import (
1515
// ScanGit scans any git source.
1616
func (e *Engine) ScanGit(ctx context.Context, c sources.GitConfig) (sources.JobProgressRef, error) {
1717
connection := &sourcespb.Git{
18-
Head: c.HeadRef,
19-
Base: c.BaseRef,
20-
Bare: c.Bare,
21-
Uri: c.URI,
22-
ExcludeGlobs: c.ExcludeGlobs,
23-
IncludePathsFile: c.IncludePathsFile,
24-
ExcludePathsFile: c.ExcludePathsFile,
25-
MaxDepth: int64(c.MaxDepth),
26-
SkipBinaries: c.SkipBinaries,
27-
ClonePath: c.ClonePath,
28-
NoCleanup: c.NoCleanup,
29-
PrintLegacyJson: c.PrintLegacyJSON,
18+
Head: c.HeadRef,
19+
Base: c.BaseRef,
20+
Bare: c.Bare,
21+
Uri: c.URI,
22+
ExcludeGlobs: c.ExcludeGlobs,
23+
IncludePathsFile: c.IncludePathsFile,
24+
ExcludePathsFile: c.ExcludePathsFile,
25+
MaxDepth: int64(c.MaxDepth),
26+
SkipBinaries: c.SkipBinaries,
27+
ClonePath: c.ClonePath,
28+
NoCleanup: c.NoCleanup,
29+
PrintLegacyJson: c.PrintLegacyJSON,
30+
TrustLocalGitConfig: c.TrustLocalGitConfig,
3031
}
3132

3233
var conn anypb.Any

pkg/engine/git_test.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package engine
22

33
import (
4+
"net/url"
45
"os"
6+
"os/exec"
7+
"path/filepath"
58
"runtime"
69
"testing"
10+
"time"
711

812
"github.com/stretchr/testify/assert"
913

1014
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
1115
"github.com/trufflesecurity/trufflehog/v3/pkg/decoders"
1216
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
1317
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/defaults"
18+
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
1419
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
1520
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
1621
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/git"
@@ -32,7 +37,7 @@ func (p *discardPrinter) Print(context.Context, *detectors.ResultWithMetadata) e
3237
func TestGitEngine(t *testing.T) {
3338
ctx := context.Background()
3439
repoUrl := "https://github.com/dustin-decker/secretsandstuff.git"
35-
path, _, err := git.PrepareRepo(ctx, repoUrl, "")
40+
path, _, err := git.PrepareRepo(ctx, repoUrl, "", false, false)
3641
if err != nil {
3742
t.Error(err)
3843
}
@@ -118,6 +123,78 @@ func TestGitEngine(t *testing.T) {
118123
}
119124
}
120125

126+
func TestGitEngineWithMirrorAndBareClones(t *testing.T) {
127+
ctx := context.Background()
128+
129+
parent, err := os.MkdirTemp("", "trufflehog-test-keys-*")
130+
if err != nil {
131+
t.Fail()
132+
}
133+
defer os.RemoveAll(parent)
134+
localRepo := filepath.Join(parent, "test_keys.git")
135+
cloneCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
136+
defer cancel()
137+
138+
// clone with --mirror and --bare from https://github.com/trufflesecurity/test_keys.git to local and then pass it in as a local path
139+
cloneCmd := exec.CommandContext(cloneCtx, "git", "clone", "--mirror", "--bare", "https://github.com/trufflesecurity/test_keys.git", localRepo)
140+
if _, err := cloneCmd.CombinedOutput(); err != nil {
141+
t.Fail()
142+
}
143+
144+
fileURI := (&url.URL{Scheme: "file", Path: filepath.ToSlash(localRepo)}).String()
145+
146+
run := func(t *testing.T, mirror bool, cfg sources.GitConfig) (uint64, uint64) {
147+
t.Helper()
148+
149+
const defaultOutputBufferSize = 64
150+
opts := []func(*sources.SourceManager){
151+
sources.WithSourceUnits(),
152+
sources.WithBufferedOutput(defaultOutputBufferSize),
153+
}
154+
sourceManager := sources.NewManager(opts...)
155+
156+
conf := Config{
157+
Concurrency: 1,
158+
Decoders: decoders.DefaultDecoders(),
159+
Detectors: defaults.DefaultDetectors(),
160+
Verify: false, // avoid network-dependent verification in tests
161+
SourceManager: sourceManager,
162+
Dispatcher: NewPrinterDispatcher(new(discardPrinter)),
163+
}
164+
165+
feature.UseGitMirror.Store(false)
166+
if mirror {
167+
feature.UseGitMirror.Store(true)
168+
defer feature.UseGitMirror.Store(false)
169+
}
170+
171+
e, err := NewEngine(ctx, &conf)
172+
assert.NoError(t, err)
173+
174+
e.Start(ctx)
175+
_, err = e.ScanGit(ctx, cfg)
176+
assert.NoError(t, err)
177+
assert.NoError(t, e.Finish(ctx))
178+
179+
m := e.GetMetrics()
180+
secrets := m.VerifiedSecretsFound + m.UnverifiedSecretsFound
181+
bytes := m.BytesScanned
182+
return secrets, bytes
183+
}
184+
185+
s1, b1 := run(t, true, sources.GitConfig{URI: "https://github.com/trufflesecurity/test_keys.git"})
186+
s2, b2 := run(t, false, sources.GitConfig{URI: fileURI, Bare: true})
187+
s3, b3 := run(t, false, sources.GitConfig{URI: fileURI, Bare: true, TrustLocalGitConfig: true})
188+
189+
assert.Greater(t, int(s1), 0)
190+
assert.Greater(t, int(b1), 0)
191+
192+
assert.Equal(t, s1, s2)
193+
assert.Equal(t, s1, s3)
194+
assert.Equal(t, b1, b2)
195+
assert.Equal(t, b1, b3)
196+
}
197+
121198
func BenchmarkGitEngine(b *testing.B) {
122199
ctx := context.Background()
123200
repoUrl := "https://github.com/dustin-decker/secretsandstuff.git"

0 commit comments

Comments
 (0)