Files
trufflehog/pkg/sources/docker/docker_test.go
Tanner Jones 053617c834 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
2025-06-06 14:29:22 -04:00

238 lines
6.2 KiB
Go

package docker
import (
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/anypb"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/credentialspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
func TestDockerImageScan(t *testing.T) {
dockerConn := &sourcespb.Docker{
Credential: &sourcespb.Docker_Unauthenticated{
Unauthenticated: &credentialspb.Unauthenticated{},
},
Images: []string{"trufflesecurity/secrets"},
}
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)
var wg sync.WaitGroup
chunksChan := make(chan *sources.Chunk, 1)
chunkCounter := 0
layerCounter := 0
historyCounter := 0
wg.Add(1)
go func() {
defer wg.Done()
for chunk := range chunksChan {
assert.NotEmpty(t, chunk)
chunkCounter++
if isHistoryChunk(t, chunk) {
historyCounter++
} else {
layerCounter++
}
}
}()
err = s.Chunks(context.TODO(), chunksChan)
assert.NoError(t, err)
close(chunksChan)
wg.Wait()
assert.Equal(t, 2, chunkCounter)
assert.Equal(t, 1, layerCounter)
assert.Equal(t, 1, historyCounter)
}
func TestDockerImageScanWithDigest(t *testing.T) {
dockerConn := &sourcespb.Docker{
Credential: &sourcespb.Docker_Unauthenticated{
Unauthenticated: &credentialspb.Unauthenticated{},
},
Images: []string{"trufflesecurity/secrets@sha256:864f6d41209462d8e37fc302ba1532656e265f7c361f11e29fed6ca1f4208e11"},
}
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)
var wg sync.WaitGroup
chunksChan := make(chan *sources.Chunk, 1)
chunkCounter := 0
layerCounter := 0
historyCounter := 0
var historyChunk *source_metadatapb.Docker
var layerChunk *source_metadatapb.Docker
wg.Add(1)
go func() {
defer wg.Done()
for chunk := range chunksChan {
assert.NotEmpty(t, chunk)
chunkCounter++
if isHistoryChunk(t, chunk) {
// save last for later comparison
historyChunk = chunk.SourceMetadata.GetDocker()
historyCounter++
} else {
layerChunk = chunk.SourceMetadata.GetDocker()
layerCounter++
}
}
}()
err = s.Chunks(context.TODO(), chunksChan)
assert.NoError(t, err)
close(chunksChan)
wg.Wait()
// Since this test pins the layer by digest, layers will have consistent
// hashes. This allows layer digest comparison as they will be stable for
// given image digest.
assert.Equal(t, &source_metadatapb.Docker{
Image: "trufflesecurity/secrets",
Tag: "sha256:864f6d41209462d8e37fc302ba1532656e265f7c361f11e29fed6ca1f4208e11",
File: "image-metadata:history:0:created-by",
Layer: "sha256:a794864de8c4ff087813fd66cff74601b84cbef8fe1a1f17f9923b40cf051b59",
}, historyChunk)
assert.Equal(t, &source_metadatapb.Docker{
Image: "trufflesecurity/secrets",
Tag: "sha256:864f6d41209462d8e37fc302ba1532656e265f7c361f11e29fed6ca1f4208e11",
File: "/aws",
Layer: "sha256:a794864de8c4ff087813fd66cff74601b84cbef8fe1a1f17f9923b40cf051b59",
}, layerChunk)
assert.Equal(t, 2, chunkCounter)
assert.Equal(t, 1, layerCounter)
assert.Equal(t, 1, historyCounter)
}
func TestBaseAndTagFromImage(t *testing.T) {
tests := []struct {
image string
wantBase string
wantTag string
wantDigest bool
}{
{"golang:1.16", "golang", "1.16", false},
{"golang@sha256:abcdef", "golang", "sha256:abcdef", true},
{"ghcr.io/golang:1.16", "ghcr.io/golang", "1.16", false},
{"ghcr.io/golang:nightly", "ghcr.io/golang", "nightly", false},
{"ghcr.io/golang", "ghcr.io/golang", "latest", false},
{"ghcr.io/trufflesecurity/secrets", "ghcr.io/trufflesecurity/secrets", "latest", false},
}
for _, tt := range tests {
gotBase, gotTag, gotDigest := baseAndTagFromImage(tt.image)
if gotBase != tt.wantBase || gotTag != tt.wantTag || gotDigest != tt.wantDigest {
t.Errorf("baseAndTagFromImage(%q) = (%q, %q, %v), want (%q, %q, %v)",
tt.image, gotBase, gotTag, gotDigest, tt.wantBase, tt.wantTag, tt.wantDigest)
}
}
}
func isHistoryChunk(t *testing.T, chunk *sources.Chunk) bool {
t.Helper()
metadata := chunk.SourceMetadata.GetDocker()
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")
}