trufflehog/pkg/sources/docker/docker_test.go
James Telfer 0024b6ce77
feat: support docker image history scanning (#2882)
* feat: support docker image history scanning

* refactor: collapse error handling into return

Style suggestion from review feedback.

* fix: associate layers with history entries

Where possible, add the associated layer to the history entry record. This may help tracing any issues discovered.

This also changes the entry reference format to `image-metadata:history:%d:created-by` which _may_ be more self-explanatory.
2024-05-28 14:07:43 -07:00

168 lines
4.5 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:")
}