Skip to content

Commit 053617c

Browse files
feat(docker): implement exclude paths functionality (#4057)
Description: Add support for excluding paths in Docker source scanning: Add ExcludePaths field to Docker protobuf Implement path exclusion logic in docker.go Add comprehensive test coverage for exact and wildcard path matching Update engine to pass exclude paths configuration Add CLI support for --exclude-paths flag The implementation supports: Exact path matching (e.g., /var/log/test) Wildcard path matching (e.g., /var/log/test/*) Multiple exclude paths Tests ensure proper handling of: Exact path exclusions Wildcard exclusions Edge cases and similar paths References: https://github.com/trufflesecurity/trufflehog/issues/2216?utm_source=chatgpt.com
1 parent 3fbb9e9 commit 053617c

File tree

7 files changed

+740
-620
lines changed

7 files changed

+740
-620
lines changed

main.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,10 @@ var (
183183
circleCiScan = cli.Command("circleci", "Scan CircleCI")
184184
circleCiScanToken = circleCiScan.Flag("token", "CircleCI token. Can also be provided with environment variable").Envar("CIRCLECI_TOKEN").Required().String()
185185

186-
dockerScan = cli.Command("docker", "Scan Docker Image")
187-
dockerScanImages = dockerScan.Flag("image", "Docker image to scan. Use the file:// prefix to point to a local tarball, otherwise a image registry is assumed.").Required().Strings()
188-
dockerScanToken = dockerScan.Flag("token", "Docker bearer token. Can also be provided with environment variable").Envar("DOCKER_TOKEN").String()
186+
dockerScan = cli.Command("docker", "Scan Docker Image")
187+
dockerScanImages = dockerScan.Flag("image", "Docker image to scan. Use the file:// prefix to point to a local tarball, otherwise a image registry is assumed.").Required().Strings()
188+
dockerScanToken = dockerScan.Flag("token", "Docker bearer token. Can also be provided with environment variable").Envar("DOCKER_TOKEN").String()
189+
dockerExcludePaths = dockerScan.Flag("exclude-paths", "Comma separated list of paths to exclude from scan").String()
189190

190191
travisCiScan = cli.Command("travisci", "Scan TravisCI")
191192
travisCiScanToken = travisCiScan.Flag("token", "TravisCI token. Can also be provided with environment variable").Envar("TRAVISCI_TOKEN").Required().String()
@@ -873,6 +874,7 @@ func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics,
873874
BearerToken: *dockerScanToken,
874875
Images: *dockerScanImages,
875876
UseDockerKeychain: *dockerScanToken == "",
877+
ExcludePaths: strings.Split(*dockerExcludePaths, ","),
876878
}
877879
if ref, err := eng.ScanDocker(ctx, cfg); err != nil {
878880
return scanMetrics, fmt.Errorf("failed to scan Docker: %v", err)

pkg/engine/docker.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import (
1414

1515
// ScanDocker scans a given docker connection.
1616
func (e *Engine) ScanDocker(ctx context.Context, c sources.DockerConfig) (sources.JobProgressRef, error) {
17-
connection := &sourcespb.Docker{Images: c.Images}
17+
connection := &sourcespb.Docker{
18+
Images: c.Images,
19+
ExcludePaths: c.ExcludePaths,
20+
}
1821

1922
switch {
2023
case c.UseDockerKeychain:

pkg/pb/sourcespb/sources.pb.go

Lines changed: 626 additions & 616 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/sources/docker/docker.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"google.golang.org/protobuf/types/known/anypb"
2222

2323
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
24+
"github.com/trufflesecurity/trufflehog/v3/pkg/common/glob"
2425
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
2526
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
2627
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
@@ -36,6 +37,7 @@ type Source struct {
3637
verify bool
3738
concurrency int
3839
conn sourcespb.Docker
40+
globFilter *glob.Filter
3941
sources.Progress
4042
sources.CommonSourceUnitUnmarshaller
4143
}
@@ -74,6 +76,15 @@ func (s *Source) Init(_ context.Context, name string, jobId sources.JobID, sourc
7476
return fmt.Errorf("error unmarshalling connection: %w", err)
7577
}
7678

79+
// Extract exclude paths from connection and compile regexes
80+
if paths := s.conn.GetExcludePaths(); len(paths) > 0 {
81+
var err error
82+
s.globFilter, err = glob.NewGlobFilter(glob.WithExcludeGlobs(paths...))
83+
if err != nil {
84+
return fmt.Errorf("error creating glob filter for exclude paths: %w", err)
85+
}
86+
}
87+
7788
return nil
7889
}
7990

@@ -349,6 +360,12 @@ func (s *Source) processChunk(ctx context.Context, info chunkProcessingInfo, chu
349360
return nil
350361
}
351362

363+
// Check if the file should be excluded
364+
filePath := "/" + info.name
365+
if s.isExcluded(ctx, filePath) {
366+
return nil
367+
}
368+
352369
chunkReader := sources.NewChunkReader()
353370
chunkResChan := chunkReader(ctx, info.reader)
354371

@@ -384,6 +401,22 @@ func (s *Source) processChunk(ctx context.Context, info chunkProcessingInfo, chu
384401
return nil
385402
}
386403

404+
// isExcluded checks if a given filePath should be excluded based on the configured excludePaths and excludeRegexes.
405+
func (s *Source) isExcluded(ctx context.Context, filePath string) bool {
406+
if s.globFilter == nil {
407+
return false // No filter configured, so nothing is excluded.
408+
}
409+
// globFilter.ShouldInclude returns true if it's NOT excluded by an exclude glob or if it IS included by an include glob.
410+
// If ShouldInclude is true (passes the filter), it means it was NOT matched by an exclude glob, so it's NOT excluded.
411+
// If ShouldInclude is false (fails the filter), it means it WAS matched by an exclude glob, so it IS excluded.
412+
isIncluded := s.globFilter.ShouldInclude(filePath)
413+
414+
if !isIncluded {
415+
ctx.Logger().V(2).Info("skipping file: matches an exclude pattern", "file", filePath, "configured_exclude_paths", s.conn.GetExcludePaths())
416+
}
417+
return !isIncluded
418+
}
419+
387420
func (s *Source) remoteOpts() ([]remote.Option, error) {
388421
defaultTransport := &http.Transport{
389422
Proxy: http.ProxyFromEnvironment,

pkg/sources/docker/docker_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,72 @@ func isHistoryChunk(t *testing.T, chunk *sources.Chunk) bool {
166166
return metadata != nil &&
167167
strings.HasPrefix(metadata.File, "image-metadata:history:")
168168
}
169+
170+
func TestDockerScanWithExclusions(t *testing.T) {
171+
dockerConn := &sourcespb.Docker{
172+
Credential: &sourcespb.Docker_Unauthenticated{
173+
Unauthenticated: &credentialspb.Unauthenticated{},
174+
},
175+
Images: []string{"trufflesecurity/secrets@sha256:864f6d41209462d8e37fc302ba1532656e265f7c361f11e29fed6ca1f4208e11"},
176+
ExcludePaths: []string{"/aws", "/gcp*", "/exactmatch"},
177+
}
178+
179+
conn := &anypb.Any{}
180+
err := conn.MarshalFrom(dockerConn)
181+
assert.NoError(t, err)
182+
183+
s := &Source{}
184+
err = s.Init(context.TODO(), "test source", 0, 0, false, conn, 1)
185+
assert.NoError(t, err)
186+
187+
// Test cases for exclusion logic
188+
testCases := []struct {
189+
name string
190+
path string
191+
expected bool
192+
}{
193+
{"excluded_exact", "/aws", true},
194+
{"excluded_wildcard", "/gcp/something", true},
195+
{"excluded_exact_match_file", "/exactmatch", true},
196+
{"not_excluded", "/azure", false},
197+
{"gcp_root_should_be_excluded_by_gcp_star", "/gcp", true},
198+
}
199+
200+
for _, tc := range testCases {
201+
t.Run(tc.name, func(t *testing.T) {
202+
assert.Equal(t, tc.expected, s.isExcluded(context.TODO(), tc.path))
203+
})
204+
}
205+
206+
// Keep the original test structure to ensure Chunks processing respects exclusions
207+
var wg sync.WaitGroup
208+
chunksChan := make(chan *sources.Chunk, 1)
209+
foundExcludedPath := false
210+
211+
wg.Add(1)
212+
go func() {
213+
defer wg.Done()
214+
for chunk := range chunksChan {
215+
// Skip history chunks
216+
if isHistoryChunk(t, chunk) {
217+
continue
218+
}
219+
220+
metadata := chunk.SourceMetadata.GetDocker()
221+
assert.NotNil(t, metadata)
222+
223+
// Check if we found a chunk with the excluded path
224+
if metadata.File == "/aws" {
225+
foundExcludedPath = true
226+
}
227+
}
228+
}()
229+
230+
err = s.Chunks(context.TODO(), chunksChan)
231+
assert.NoError(t, err)
232+
233+
close(chunksChan)
234+
wg.Wait()
235+
236+
assert.False(t, foundExcludedPath, "Found a chunk that should have been excluded")
237+
}

pkg/sources/sources.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ type DockerConfig struct {
227227
BearerToken string
228228
// UseDockerKeychain determines whether to use the Docker keychain.
229229
UseDockerKeychain bool
230+
// ExcludePaths is a list of paths to exclude from scanning.
231+
ExcludePaths []string
230232
}
231233

232234
// GCSConfig defines the optional configuration for a GCS source.

proto/sources.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ message Docker {
158158
bool docker_keychain = 4;
159159
}
160160
repeated string images = 5;
161+
repeated string exclude_paths = 6;
161162
}
162163

163164
message ECR {

0 commit comments

Comments
 (0)