Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,10 @@ var (
circleCiScan = cli.Command("circleci", "Scan CircleCI")
circleCiScanToken = circleCiScan.Flag("token", "CircleCI token. Can also be provided with environment variable").Envar("CIRCLECI_TOKEN").Required().String()

dockerScan = cli.Command("docker", "Scan Docker Image")
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()
dockerScanToken = dockerScan.Flag("token", "Docker bearer token. Can also be provided with environment variable").Envar("DOCKER_TOKEN").String()
dockerScan = cli.Command("docker", "Scan Docker Image")
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()
dockerScanToken = dockerScan.Flag("token", "Docker bearer token. Can also be provided with environment variable").Envar("DOCKER_TOKEN").String()
dockerExcludePaths = dockerScan.Flag("exclude-paths", "Comma separated list of paths to exclude from scan").String()

travisCiScan = cli.Command("travisci", "Scan TravisCI")
travisCiScanToken = travisCiScan.Flag("token", "TravisCI token. Can also be provided with environment variable").Envar("TRAVISCI_TOKEN").Required().String()
Expand Down Expand Up @@ -873,6 +874,7 @@ func runSingleScan(ctx context.Context, cmd string, cfg engine.Config) (metrics,
BearerToken: *dockerScanToken,
Images: *dockerScanImages,
UseDockerKeychain: *dockerScanToken == "",
ExcludePaths: strings.Split(*dockerExcludePaths, ","),
}
if ref, err := eng.ScanDocker(ctx, cfg); err != nil {
return scanMetrics, fmt.Errorf("failed to scan Docker: %v", err)
Expand Down
5 changes: 4 additions & 1 deletion pkg/engine/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import (

// ScanDocker scans a given docker connection.
func (e *Engine) ScanDocker(ctx context.Context, c sources.DockerConfig) (sources.JobProgressRef, error) {
connection := &sourcespb.Docker{Images: c.Images}
connection := &sourcespb.Docker{
Images: c.Images,
ExcludePaths: c.ExcludePaths,
}

switch {
case c.UseDockerKeychain:
Expand Down
1,242 changes: 626 additions & 616 deletions pkg/pb/sourcespb/sources.pb.go

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions pkg/sources/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"google.golang.org/protobuf/types/known/anypb"

"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/common/glob"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
Expand All @@ -36,6 +37,7 @@ type Source struct {
verify bool
concurrency int
conn sourcespb.Docker
globFilter *glob.Filter
sources.Progress
sources.CommonSourceUnitUnmarshaller
}
Expand Down Expand Up @@ -74,6 +76,15 @@ func (s *Source) Init(_ context.Context, name string, jobId sources.JobID, sourc
return fmt.Errorf("error unmarshalling connection: %w", err)
}

// Extract exclude paths from connection and compile regexes
if paths := s.conn.GetExcludePaths(); len(paths) > 0 {
var err error
s.globFilter, err = glob.NewGlobFilter(glob.WithExcludeGlobs(paths...))
if err != nil {
return fmt.Errorf("error creating glob filter for exclude paths: %w", err)
}
}

return nil
}

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

// Check if the file should be excluded
filePath := "/" + info.name
if s.isExcluded(ctx, filePath) {
return nil
}

chunkReader := sources.NewChunkReader()
chunkResChan := chunkReader(ctx, info.reader)

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

// isExcluded checks if a given filePath should be excluded based on the configured excludePaths and excludeRegexes.
func (s *Source) isExcluded(ctx context.Context, filePath string) bool {
if s.globFilter == nil {
return false // No filter configured, so nothing is excluded.
}
// globFilter.ShouldInclude returns true if it's NOT excluded by an exclude glob or if it IS included by an include glob.
// If ShouldInclude is true (passes the filter), it means it was NOT matched by an exclude glob, so it's NOT excluded.
// If ShouldInclude is false (fails the filter), it means it WAS matched by an exclude glob, so it IS excluded.
isIncluded := s.globFilter.ShouldInclude(filePath)

if !isIncluded {
ctx.Logger().V(2).Info("skipping file: matches an exclude pattern", "file", filePath, "configured_exclude_paths", s.conn.GetExcludePaths())
}
return !isIncluded
}

func (s *Source) remoteOpts() ([]remote.Option, error) {
defaultTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Expand Down
69 changes: 69 additions & 0 deletions pkg/sources/docker/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,72 @@ func isHistoryChunk(t *testing.T, chunk *sources.Chunk) bool {
return metadata != nil &&
strings.HasPrefix(metadata.File, "image-metadata:history:")
}

func TestDockerScanWithExclusions(t *testing.T) {
dockerConn := &sourcespb.Docker{
Credential: &sourcespb.Docker_Unauthenticated{
Unauthenticated: &credentialspb.Unauthenticated{},
},
Images: []string{"trufflesecurity/secrets@sha256:864f6d41209462d8e37fc302ba1532656e265f7c361f11e29fed6ca1f4208e11"},
ExcludePaths: []string{"/aws", "/gcp*", "/exactmatch"},
}

conn := &anypb.Any{}
err := conn.MarshalFrom(dockerConn)
assert.NoError(t, err)

s := &Source{}
err = s.Init(context.TODO(), "test source", 0, 0, false, conn, 1)
assert.NoError(t, err)

// Test cases for exclusion logic
testCases := []struct {
name string
path string
expected bool
}{
{"excluded_exact", "/aws", true},
{"excluded_wildcard", "/gcp/something", true},
{"excluded_exact_match_file", "/exactmatch", true},
{"not_excluded", "/azure", false},
{"gcp_root_should_be_excluded_by_gcp_star", "/gcp", true},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, s.isExcluded(context.TODO(), tc.path))
})
}

// Keep the original test structure to ensure Chunks processing respects exclusions
var wg sync.WaitGroup
chunksChan := make(chan *sources.Chunk, 1)
foundExcludedPath := false

wg.Add(1)
go func() {
defer wg.Done()
for chunk := range chunksChan {
// Skip history chunks
if isHistoryChunk(t, chunk) {
continue
}

metadata := chunk.SourceMetadata.GetDocker()
assert.NotNil(t, metadata)

// Check if we found a chunk with the excluded path
if metadata.File == "/aws" {
foundExcludedPath = true
}
}
}()

err = s.Chunks(context.TODO(), chunksChan)
assert.NoError(t, err)

close(chunksChan)
wg.Wait()

assert.False(t, foundExcludedPath, "Found a chunk that should have been excluded")
}
2 changes: 2 additions & 0 deletions pkg/sources/sources.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ type DockerConfig struct {
BearerToken string
// UseDockerKeychain determines whether to use the Docker keychain.
UseDockerKeychain bool
// ExcludePaths is a list of paths to exclude from scanning.
ExcludePaths []string
}

// GCSConfig defines the optional configuration for a GCS source.
Expand Down
1 change: 1 addition & 0 deletions proto/sources.proto
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ message Docker {
bool docker_keychain = 4;
}
repeated string images = 5;
repeated string exclude_paths = 6;
}

message ECR {
Expand Down